算法与数据结构
树
树在计算机科学中,是一种十分基础的数据结构。几乎所有操作系统都将文件存放在树状结构中;几乎所有的编译器都要实现一个表达式树;文件压缩所用到的哈夫曼算法(Huffman’s Algorithm)需要用到树状结构;数据库所使用的B+tree则是一种相当复杂的树状结构。
二叉树
二叉树是n个节点的有限集合,该集合或者为空集,或者由一个根节点和两根互不相交的、分别被称为根节点的左子树和右子树组成。
特点:
- 每个节点最多有两个子树,所以二叉树不存在出度大于2的结点
- 左子树和右子树是由顺序的,次序不能颠倒
- 即使树中某节点只有一棵子树,也要区分左子树还是右子树
二叉搜索树
可以提供对数时间O(logn)的元素插入和访问,二叉搜索树的放置规则为:任何节点的键值一定大于其左子树中的每个节点的键值,并小于其右子树中的每个节点的键值。
- 应用场景
- 哈夫曼编码
- 海量数据并发查询
- stl容器中的set,multiset,map,multimap内部结构
平衡二叉搜索树
平衡的大致意义为:没有任何一个节点过深(深度过大)。
AVL
- 一种自平衡的二叉查找树
- AVL树中任何节点的两个子树的高度最大差是1,所以是完全平衡的
- 查找、插入和删除的平均和最坏情况都是O(logn)
- 经过修改的二叉树可能失去平衡状态(即左右高差大于1),就不是一棵AVL树了,需要将其进行旋转处理,使之重新平衡
为什么AVL的高度差不超过1
- 平衡二叉树是在二叉排序树的基础上引入的,就是为了解决二叉排序树的不平衡性导致的时间复杂度大大下降,不超过1的平衡二叉树就保持住了BST的最好时间复杂度O(logn)
红黑树
规则:
- 每个节点不是红色就是黑色
- 根节点为黑色
- 如果节点为红色,其子节点必须为黑色
- 任一节点至NULL(树尾端)的任何路径,所含的黑色节点数必须相同
特点:
- 只追求近似平衡(节点最大的深度<=最小深度*2)
- 在插入和删除节点时,翻转次数远小于平衡树,因此在需要较多插入删除操作时,使用红黑树更好。
- 在查询操作比较多时,使用平衡树比较好,因为红黑树查询的深度可能会大于平衡二叉树
B树,B+树
- 一种多路查找树
- B树中所有节点都有指向记录的指针,在非叶子节点种出现的索引项不会出现在叶子节点中;B+树种只有叶子节点会带有指向记录的指针。
- B+树的所有叶子节点通过双向链表连接在一起;B树不连接
- B树优点:对于在内部节点的数据,可以直接获得,不必根据叶子节点来定位
- B+树优点:1非叶子节点不会带上指向记录的指针,这样一个块中就可以容纳更多的索引项;2叶子节点通过指针连接在一起,可以横向遍历。
R树,R+树
R树是一种多级平衡树,它是B树在多维空间上的扩展。在R树中存放的数据并不是原始数据,而是这些数据的最小边界矩形(MBR),空间对象的MBR被包含于R树的叶结点中。在R树空间索引中,设计一些虚拟的矩形目标,将一些空间位置相近的目标,包含在这个矩形内,这些虚拟的矩形作为空间索引,它含有所包含的空间对象的指针。虚拟矩形还可以进一步细分,即可以再套虚拟矩形形成多级空间索引。
在R树的构造中,要求虚拟矩形一般尽可能少地重叠,并且一个空间对通常仅被一个虚拟矩形所包含。但空间对象千姿百态,它们的最小矩形范围经常重叠。 R+ 改进R树的空间索引,为了平衡,它允许虚拟矩形相互重叠,并允许一个空间目标被多个虚拟矩形所包含。
heap
堆实际是一个完全二叉树,所有元素都可以将其保存在一个vector之中,heap没有迭代器。
max-heap的最大值在根节点,即在vector的第一个元素;min-heap的最小值在根节点,即在vector的第一个元素。
- 插入:先将元素插入到vector的尾部,然后进行上溯程序,将新节点与其父节点比较,如果键值大于父节点,就对换位置,否则一直上溯,直到不需对换或者到达根节点。
- 弹出:弹出操作取走根节点(实现中其实时将其移至底层容器vector的最后一个元素)之后,为了满了完全二叉树的特性,必须将最下一层最右边的叶子节点拿掉,然后为其找一个适当的位置。
- 执行下溯操作,即将根节点填入根节点,然后将它与它的两个子节点作比较,并于较大的键值对调位置,一直下放到他的键值大于左右两个子节点或者到达叶子节点为止。
hash
哈希表
- 哈希表(HashTable)也叫散列表,是根据关键码值(key-value)而直接访问的数据结构
- 可以把关键码值映射到表中的一个位置来访问记录,加快查找的速度
- 这个映射函数叫做哈希函数(散列函数),存放记录的数组叫做哈希表
storage location(bucket)=H(key)
,H()
被称为哈希函数(散列函数),采用哈希技术将记录存储在一块连续的存储空间
哈希表的存取
- 存值:把key通过一个固定的哈希函数转换为一个整形数字,然后对该数字长度进行取余,取余结果当作数组的下标,将value存储在以该数字为下标的数组空间中
- 取值:再次使用哈希函数将key转换为对应的数组下标,并定位到哈希表中获取value
哈希的应用
- 加密算法:把不同长度的信息转换为杂乱的128位的编码(MD5)
- 查找:当知道key值之后,可以直接计算出元素在集合中的位置
常见的哈希函数
- 直接定址法:取关键字key或者关键字中的某个线性函数值作为散列地址,即
H(key)=key or H(key)=a*key+b; a,b=常数
- 除留余数法:取关键字key被某个不大于哈希表大小m的数p求余
- 数字分析法:当关键字位数大于地址位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为哈希地址
- 平方取中法:计算关键字值的平方,然后取平均值中间几位作为哈希地址
- 折叠法:将关键字分为位数相同的及部分,然后取这及部分的叠加和(进位舍去)作为哈希地址
哈希冲突
-
不同关键字通过相同哈希函数计算处相同的哈希地址,这种现象称为哈希冲突
-
处理方法:
- 链地址法:key相同的使用单链表连接(数组+链表)
- 开放定址法:
- 线性探测法:放到
H(key)
的下一个位置,Hi=(H(key)+i)%m
- 二次探测法:放到计算位置后偏移量(1,2,3…)的二次方地址处
- 随机探测法:
Hi=(H(key)+random)%m
-
STL中通常使用链地址法解决哈希冲突,使用的节点结构体为
-
template <class Value>
struct __hashtable_node{
__hashtable_node* next;
Value val;
};
STL中的hash表
stl中的hash_table是用于实现无序关联容器的底层结构。
其中哈希表里的元素节点被称为桶(bucket),桶内维护的是链表,但不是STL中的链表,而是自行维护的一个node。bucket聚合体也即hash_table使用vector实现。
- hash_table扩容时发生什么:
- 创建一个新桶,该桶是原来桶两倍大最接近的质数(判断质数:用n除以2到
sqrt(n)
的所有整数)
- 将原来桶里的数通过指针的转换,插入到新桶里
- 通过swap函数将新桶和旧桶交换,销毁旧桶
hash_map和map的区别
- map优点:有序,底层为红黑树,可以使map的很多操作以O(logN)的时间复杂度完成
- map缺点:空间占用高,因为内部使用RB-tree,虽然提高了运行效率,但每个节点都存储了父节点、孩子节点和颜色。
- map适用于:对于有顺序要求的场合
- hash_map优点:内部实现了哈希表,查找速度很快
- hash_map缺点:哈希表建立比较耗费时间
- hash_map适用于:对于查找问题,速度很高效
一致性哈希
一致性哈希(Distribute Hash Table, DHT)是一种哈希分布方式,其目的是为了客服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。
基本原理
将哈希空间[0,2^n-1], n=32
看成一个哈希环,每个服务器节点都配知道哈希环上,每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于该哈希值的服务器节点上。如下图将数据对象C插入到节点中,计算Hash()%2^32
会得到一个节点,此时他的顺时针方向第一个服务器节点Node C
,则将它放入节点Node C
中。
一致性哈希在增加(集群扩容)或者删除服务器节点(某节点宕机)时只会影响到哈希环中相邻的节点。例如下图中新增服务器节点X,只需要将在节点Node B
和节点Node X
之间的原本属于Node C
的数据对象C重新分配即可,对于其他数据对象A,B,D均没有影响。
一致性哈希中的数据倾斜问题
当一致性Hash算法中的服务器节点过少时,容易因为分布不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)的问题。如图中,只有两台机器D0, D1
,这两台机器离的很近,那么顺时针第一个机器节点上将存有大量的数据;第二台机器上的数据很少。
- 使用虚拟节点解决数据倾斜问题,也就是每台机器节点会进行多次哈希,最终每个机器节点在哈喜欢上会有多个虚拟节点存在,使用这种方式来大大削弱甚至避免数据倾斜问题。同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“D1#1”、“D1#2”、“D1#3”三个虚拟节点的数据均定位到 D1 上。
排序算法
算法分类
十种常见排序算法可以分为两大类:
算法复杂度
归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
动图演示
算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
动图演示
堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
动图演示
计数排序(Counting Sort)
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
动图演示
桶排序(Bucket Sort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
图片演示