春季总结(三)

目录

1、设计模式

介绍

2、零碎点

forward_list

函数传值,传引用,传地址

struct 和union

三、密码学

multimap容器

完美转发

互斥锁

数据结构之树

搜索树/排序树/查找树/BST

平衡树/AVL

B+树

红黑树

完全二叉树

 Trie(前缀树/字典树)及其应用

最小/大堆

双缓冲

数据库

top K问题


1、设计模式

工厂模式

意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

主要解决:主要解决接口选择的问题。

何时使用:我们明确地计划不同条件下创建不同实例时。

如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。

关键代码:创建过程在其子类执行。

注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。

桥接模式

桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。

介绍

意图:将抽象部分与实现部分分离,使它们都可以独立的变化。

主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。

何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。

如何解决:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。

关键代码:抽象类依赖实现类。

优点: 1、抽象和实现的分离。 2、优秀的扩展能力。 3、实现细节对客户透明。

使用场景: 1、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。 2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。

注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。

单例模式

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。可以提前进行初始化。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

2、零碎点

常见C++笔试面试题整理_ljh0302的博客-CSDN博客_c++ 面试题

【c++】经典面试题吐血整理_vector<>的博客-CSDN博客_c++编程面试题

3、结构体struct和共同体union(联合)的区别

结构体:将不同类型的数据组合成一个整体,是自定义类型

共同体:不同类型的几个变量共同占用一段内存

1)结构体中的每个成员都有自己独立的地址,它们是同时存在的;

共同体中的所有成员占用同一段内存,它们不能同时存在;

2)sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度、

#pragma pack (push,1)

这是给编译器用的参数设置,有关结构体字节对齐方式设置, #pragma pack是指定数据在内存中的对齐方式。

#pragma pack (n) 作用:C编译器将按照n个字节对齐
#pragma pack () 作用:取消自定义字节对齐方式。

#pragma pack (push,1) 作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐

#pragma pack(pop) 作用:恢复对齐状态
————————————————

5、重载overload,覆盖override,重写overwrite,这三者之间的区别

1)overload,将语义相近的几个函数用同一个名字表示,但是参数和返回值不同,这就是函数重载

特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无

2)override,派生类覆盖基类的虚函数,实现接口的重用

特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)

3)overwrite,派生类屏蔽了其同名的基类函数

特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字
————————————————

6、new、delete、malloc、free之间的关系

new/delete,malloc/free都是动态分配内存的方式

1)malloc对开辟的空间大小严格指定,而new只需要对象名

2)new为对象分配空间时,调用对象的构造函数,delete调用对象的析构函数

为什么有了malloc/free还需要new和delete

  • 因为在为对象申请空间时,除了动态内存分配,还需要调用构造函数进行初始化,不能同时把这两个工作都交给malloc,所以需要new

7、delete和delete[]的区别

delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

用new分配的内存用delete释放,用new[]分配的内存用delete[]释放


————————————————

移动构造:
  std::forward_list<int> fourth (third);            // copy constructor
  std::forward_list<int> fifth (std::move(fourth));  // move ctor. (fourth wasted)

把新创建的对象指向旧的对象,将旧的对象指向地址清空。
————————————————

forward_

list

// forward_list::before_begin
#include <iostream>
#include <forward_list>
 
int main ()
{
  std::forward_list<int> mylist = {20, 30, 40, 50};
  mylist.insert_after ( mylist.begin(), 11 ); //mylist contains: 20 11 30 40 50
  mylist.insert_after ( mylist.before_begin(), 11 ); //mylist contains: 11 20 30 40 50
  std::cout << "mylist contains:";
  for ( int& x: mylist ) std::cout << ' ' << x;
  std::cout << '\n';
  return 0;
}

// forward_list
for (forward_list<int>::iterator iter = f1.begin(); iter != f1.end(); ++iter) {
	cout << *iter << " ";		// 输出当前元素并以空格隔开
}

不支持反向遍历

增加元素

forward_list    
f1.push_front(a);    在f1的链首添加一个元素a
f1.emplace_front(a);    在f1的链首添加一个元素a
f1.emplace_after(iter,a);    在迭代器iter指向的元素之后添加一个元素a
f1.insert_after(iter,a);    在迭代器iter指向的元素之后添加一个元素a
f1.insert_after(iter,n,a);    在迭代器iter指向的元素之后添加 n 个元素a
f1.insert_after(iter,{a,b,c});    迭代器iter指向的元素之后添加大括号内的所有元素,这里添加的元素是a、b、c       

删除元素

forward_list    
f1.pop_front();    在f1的链首删除一个元素
f1.erase_after(iter);    删除迭代器iter指向的元素
f1.erase_after(iter1,iter2);    删除迭代器iter1和ter2指向的范围中元素,范围左开右开
f1.clear();    删除f1中的所有元素

查找速度非常快
————————————————

map:

优点:

有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高
缺点: 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间

适用处:对于那些有顺序要求的问题,用map会更高效一些

unordered_map:

优点: 因为内部实现了哈希表,因此其查找速度非常的快
缺点: 哈希表的建立比较耗费时间
适用处:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
总结:

内存占有率的问题就转化成红黑树 VS hash表 , 还是unorder_map占用的内存要高。
但是unordered_map执行效率要比map高很多
对于unordered_map或unordered_set容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的

hash_map和unordered_map的一次查询/插入时间为:
无冲突时:O(1) 有冲突时最坏为O(n)。

时间复杂度为常数级,一般更倾向于左边
————————————————

 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

1、构造函数不能声明为虚函数

1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

2、析构函数最好声明为虚函数

首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
————————————————

只有虚函数才使用的是动态绑定,其他的全部是静态绑定
————————————————

.volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。

volatile
场景:它用来解决变量在“共享”环境下容易出现读取错误的问题。具有以下三个特点:
易变性:
在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取
不可优化性:
volatile告诉编译器,不要对我这个变量进行各种激进的优化,保证程序员写在代码中的指令,一定会被执行(不要将a=1直接变成1)
顺序性:
volatile变量间的操作,不会被编译器交换顺序
————————————————

内联函数不能是虚函数:inline是编译器将函数内容替换到函数调用的地方,是静态编译的,但是虚函数动态调用的,是只有在函数运行的时候才能知道调用的是哪个函数,所以在编译的时候,incline它不知道是子类函数父类的虚函数,因此编译器会忽略。
————————————————

函数传值,传引用,传地址

  • 函数传值:对实参创建一份拷贝,在函数中改变这个值,实参原来的不会被改变

  • 函数传地址:对实参创建一个指针变量,在函数中改变这个值,实参也会改变

  • 函数传引用:没有创建拷贝,没有创建变量,对实参取别名之后,直接对这个量进行修改————————————————

sizeof(指针)和sizeof(数组名)的区别
区别1:   sizeof(数组名)大小是整个数组的大小,sizeof(指针)大小是4或者8
区别2:可以为指针取别名,但是不能为数组名取别名
联系:当他们两个作为实参传给指针类型的形参时,大小都为指针的大小,这也是为什么我们在用数组作为实参的时候,还会要传一个数组长度给函数。
————————————————

sizeof(字符数组)

char a[]="abc"  sizeof(a)=4,因为默认有‘\0’作为结束符

char a[3]="abc" sizeof(a)=3,固定长度,丢失‘\0’

char a[]={'a','b','c'} sizeof(a)=3 丢失‘\0’

sizeof(类)

类的大小只与:非static成员变量以及虚函数有关

理由:static不是类的某一个实例自己独有的

构造函数或析构函数

1.空类:1

2.含有虚函数的类:4

3.含有变量的类:取最大的那个

4.含有普通函数:1(成员函数不消耗空间)
————————————————
一、数组或字符串的长度:sizeof()、strlen()
1、sizeof():返回所占总空间的字节数
(1)、对于整型字符型数组
(2)、对于整型或字符型指针
2、strlen():返回字符数组或字符串所占的字节数
(1)、针对字符数组
(2)、针对字符指针

char ss[100] = "0123456789";
sizeof(ss)为100,ss表示在内存中预分配的大小,100*1
strlen(ss)为10,它的内部实现用一个循环计算字符串的长度,直到'\0'为止。


二、string中length()和size()

c++中的size()和length()没有区别

不同类型的指针大小都是4 ,结构体指针的大小也为4。很合理,因为可以随意的强制转换。
————————————————

struct 和union

  • struct会给每一个成员变量分配一个内存空间,union所有的成员公用一块内存,只能存储一个被选中的成员的信息


————————————————

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend,如下所示:

class Box
{
   double width;
public:
   friend void printWidth( Box box );
   void setWidth( double wid );
};
 
// 成员函数定义
void Box::setWidth( double wid )
{
    width = wid;
}
 
// 请注意:printWidth() 不是任何类的成员函数
void printWidth( Box box )
{
   /* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
   cout << "Width of box : " << box.width <<endl;
}

三、密码学

openssl

加密

安全协议

分布式

multimap容器


multimap容器保存的是有序的键/值对,但是可以保存重复的元素。multimap中会出现具有相同键值的元素序列。multimap大部分成员函数的使用方式和map相同。因为重复键的原因,multimap有一些函数的使用方式和map有一些区别。

3.1 访问元素
multimap 不支持下标运算符,因为键并不能确定一个唯一元素。和 map 相似,multimap 也不能使用 at() 函数。

find函数

multimap 的成员函数 find() 可以返回一个键和参数匹配的元素的迭代器。例如

std::multimap<std::string, size_t> people {
    
    {"Ann",25},{"Bill", 46}, {"Jack", 77}, {"Jack", 32},{"Jill", 32}, {"Ann", 35} };
std::string name {"Bill"};
auto iter = people.find(name);
if (iter ! = std::end (people))
    std::cout << name << " is " << iter->second << std::endl;
iter = people.find ("Ann");
if (iter != std::end(people))
    std::cout << iter->first << " is " << iter->second <<std::endl;
如果没有找到键,会返回一个结束迭代器,所以我们应该总是对返回值进行检查。第一个 find() 调用的参数是一个键对象,因为这个键是存在的,所以输出语句可以执行。
第二个 find() 调用的参数是一个字符串常量,它说明参数不需要和键是相同的类型。
对容器来说,可以用任何值或对象作为参数,只要可以用函数对象将它们和键进行比较。
最后一条输出语句也可以执行,
因为有等于 "Ann" 的键。事实上,这里有两个等于 "Ann" 的键,你可能也会得到不同的运行结果。
————————————————

auto pr = people.equal_range("Ann");
if(pr.first != std::end(people))
{
    for (auto iter = pr.first ; iter != pr.second; ++iter)
        std:cout << iter->first << " is " << iter->second << std::endl;
}

equal_range() 的参数可以是和键同类型的对象,或是不同类型的但可以和键比较的对象。返回的 pair 对象的成员变量 first 是一个迭代器,它指向第一个大于等于参数的元素;如果键和参数相等的元素存在的话,它是第一个键和参数相同的元素。如果键不存在,pair 的成员变量 first 就是容器的结束迭代器,所以应该总是对它们进行捡查。

 lower_bound 和 upper_bound

multimap 的成员函数 lower_bound() 会返回一个迭代器,它指向键值和参数相等或大于参数的第一个元素,或者指向结束迭代器。upper_bound() 也返回一个迭代器,它指向键值大于函数参数的第一个元素,如果这样的元素不出现的话,它就是一个结束迭代器。所以,当存在一个或多个相等键时,这些函数会返回一个开始迭代器和一个结束迭代器,它们指定了和参数匹配的元素的范围,这和 equal_range() 返回的迭代器是相同的。因而前面的代码段可以这样重写

完美转发

完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变

template<typename T>
void print(T& t) {
	std::cout << "左值" << std::endl;
}

template<typename T>
void print(T&& t) {
	std::cout << "右值" << std::endl;
}

template<typename T>
void testForward(T&& v) {
	print(v);
	print(std::forward<T>(v));
	print(std::move(v));
}

int main(int argc, char * argv[])
{
    testForward(1);

    std::cout << "======================" << std::endl;

    int x = 1;
    testFoward(x);
}
左值
右值
右值
=========================
左值
左值
右值

从上面第一组的结果我们可以看到,传入的1虽然是右值,但经过函数传参之后它变成了左值(在内存中分配了空间);而第二行由于使用了std::forward函数,所以不会改变它的右值属性,因此会调用参数为右值引用的print模板函数;第三行,因为std::move会将传入的参数强制转成右值,所以结果一定是右值。

再来看看第二组结果。因为x变量是左值,所以第一行一定是左值;第二行使用forward处理,它依然会让其保持左值,所以第二也是左值;最后一行使用move函数,因此一定是右值。

互斥锁

数据结构之树

搜索树/排序树/查找树/BST

左节点小 右节点大

二叉排序树(Binary Search Tree),又称二叉查找树、二叉搜索树
定义:
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有键值相等的结点。

不足:

但是BST树有一个不足的地方,就是如果插入的结点的值的顺序,是越来越小或者越来越大的,那么BST就会退化为一条链表,那么其查询的时间复杂度就会降为O(n)。

平衡树/AVL

 平衡二叉树是一棵高度平衡的二叉查找树,由于BST树存在上述的不足,所以AVL树就出来了。

平衡二叉搜索树,又被称为AVL树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 —-来自百度百科

​  由于普通的二叉查找树会容易失去”平衡“,极端情况下,二叉查找树会退化成线性的链表,导致插入和查找的复杂度下降到 O(n) ,所以,这也是平衡二叉树设计的初衷。那么平衡二叉树如何保持”平衡“呢?根据定义,有两个重点,一是左右两子树的高度差的绝对值不能超过1,二是左右两子树也是一颗平衡二叉树。由此可以看出平衡二叉树是一棵高度平衡的二叉查找树。所以,要构建跟维系一棵平衡二叉树就比普通的二叉树要复杂的多。在构建一棵平衡二叉树的过程中,当有新的节点要插入时,检查是否因插入后而破坏了树的平衡,如果是,则需要做旋转去改变树的结构

左旋右旋

左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点;

  而右旋就是反过来,将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点。

  即左旋就是往左变换,右旋就是往右变换。不管是左旋还是右旋,旋转的目的都是将节点多的一支出让节点给另一个节点少的一支。

​  举个例子,像上图是否平衡二叉树的图里面,左图在没插入前”19“节点前,该树还是平衡二叉树,但是在插入”19“后,导致了”15“的左右子树失去了”平衡“,所以此时可以将”15“节点进行左旋,让”15“自身把节点出让给”17“作为”17“的左树,使得”17“节点左右子树平衡,而”15“节点没有子树,左右也平衡了。如下图,

非平衡二叉树左旋变换成平衡二叉树例图

 二叉查找树与平衡二叉树_金发只是水一下的博客-CSDN博客_平衡二叉树

B+树

B树和AVL树(平衡二叉树) 的差别就是 B树 属于多叉树,又名平衡多路查找树,即一个结点的查找路径不止左、右两个,而是有多个。数据库索引技术里大量使用者B树和B+树的数据结构。一个结点存储多个值(索引)。

B树的阶数:M阶表示 一个B树的结最多有多少个查找路径(即这个结点有多少个子节点)。M=M路,M=2是二叉树,M=3则是三叉树。

一棵M阶B树有以下特点。

特点:

1.   每个结点的值(索引) 都是按递增次序排列存放的,并遵循左小右大原则。

2.  根结点 的 子节点 个数为 [2,M]。

3. 除 根结点 以外 的 非叶子结点 的子节点个数 为[ Math.ceil(M/2),M]。 Math.ceil() 为向上取整。

4. 每个 非叶子结点 的值(索引) 个数 = 子节点个数 -1 。最小为 Math.ceil(M/2)-1   最大为 M-1 个。

5. B树的所有叶子结点都位于同一层。


B+树和B树最大的不同是:

B+树内部有两种结点,一种是索引结点,一种是叶子结点。
B+树的索引结点并不会保存记录,只用于索引,所有的数据都保存在B+树的叶子结点中。而B树则是所有结点都会保存数据。
B+树的叶子结点都会被连成一条链表。叶子本身按索引值的大小从小到大进行排序。即这条链表是 从小到大的。多了条链表方便范围查找数据。
B树的所有索引值是不会重复的,而B+树 非叶子结点的索引值 最终一定会全部出现在 叶子结点中

B树好处:

B树的每一个结点都包含key(索引值) 和 value(对应数据),因此方位离根结点近的元素会更快速。(相对于B+树)

B树的不足:

不利于范围查找(区间查找),如果要找 0~100的索引值,那么B树需要多次从根结点开始逐个查找。

而B+树由于叶子结点都有链表,且链表是以从小到大的顺序排好序的,因此可以直接通过遍历链表实现范围查找。
 

红黑树等平衡性也可以用来实现索引,但文件系统和数据库普遍采用B+树作为索引结构,原因有二:

  • 更少的查找次数
  • 利用磁盘预读特性
  • B+树还有一个最大的好处,方便扫库,B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持range-query非常方便,而B树不支持。这是数据库选用B+树的最主要原因。
  • 因为B+树的非叶子节点不用存数据,只需要存key,所以一个节点能够存储更大范围的区间。

红黑树

红黑树是一个平衡的二叉树,红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
【1】性质1. 节点是红色或黑色。
【2】性质2. 根节点是黑色。
【3】性质3 每个叶节点是黑色的。
【4】性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
【5】性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

5.红黑树相比于BST和AVL树有什么优点?

红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。

相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。

红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,但是他们的查找效率都是O(logN),所以红黑树应用还是高于AVL树的. 实际上插入 AVL 树和红黑树的速度取决于你所插入的数据.如果你的数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的

6.红黑树相对于哈希表,在选择使用的时候有什么依据?

权衡三个因素: 查找速度, 数据量, 内存使用,可扩展性。
  总体来说,hash查找速度会比map快,而且查找速度基本和数据量大小无关,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n) 小,hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash。但若你对内存使用特别严格, 希望程序尽可能少消耗内存,那么一定要小心,hash可能会让你陷入尴尬,特别是当你的hash对象特别多时,你就更无法控制了,而且 hash的构造速度较慢。

红黑树并不适应所有应用树的领域。如果数据基本上是静态的,那么让他们待在他们能够插入,并且不影响平衡的地方会具有更好的性能。如果数据完全是静态的,例如,做一个哈希表,性能可能会更好一些。

在实际的系统中,例如,需要使用动态规则的防火墙系统,使用红黑树而不是散列表被实践证明具有更好的伸缩性。Linux内核在管理vm_area_struct时就是采用了红黑树来维护内存块的。

红黑树通过扩展节点域可以在不改变时间复杂度的情况下得到结点的秩。

面试中红黑树常考问题_weixin_30788619的博客-CSDN博客

完全二叉树

完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树
    //右下节点,则 第 i = len/2-1个,c1= 2i+1,c2 = 2i+2

 
Trie(前缀树/字典树)及其应用

1,根节点不包含字符,除根节点意外每个节点只包含一个字符。

2,从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。

3,每个节点的所有子节点包含的字符串不相同。

每一个节点包含一个数组m_char[26]代表26个字母和一个标志位isend

最小/大堆


    //右下节点,则 第 i = len/2-1个,c1= 2i+1,c2 = 2i+2

双缓冲

数据库

索引原理

MyISAM索引实现
MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。

这里设表一共有三列,假设我们以Col1为主键,则图8是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:


索引会提高搜索速度,降低增删改效率。

一到算法题:

一万个数据中找到第50个大的

top K问题

    最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法的时间复杂度一般为O(nlogn),如快速排序。但是在32位的机器上,每个float类型占4个字节,1亿个浮点数就要占用400MB的存储空间,对于一些可用内存小于400M的计算机而言,很显然是不能一次将全部数据读入内存进行排序的。其实即使内存能够满足要求(我机器内存都是8GB),该方法也并不高效,因为题目的目的是寻找出最大的10000个数即可,而排序却是将所有的元素都排序了,做了很多的无用功。

        第二种方法为局部淘汰法,该方法与排序方法类似,用一个容器保存前10000个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的10000个数还小,那么容器内这个10000个数就是最大10000个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这1亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即10000。

        第三种方法是分治法,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100*10000个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^6*4=4MB,一共需要101次这样的比较。

        第四种方法是Hash法。如果这1亿个书里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的10000个数。

        第五种方法采用最小堆。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。
 

猜你喜欢

转载自blog.csdn.net/baidu_39486224/article/details/123844285