数据结构总结及应用场景
数据结构总结及应用场景
文章目录
- 1. 线性结构- 2. 树结构-
1. 线性结构
内存中数据的存储形式分为连续存储和离散存储两种,分别对应了数组和链表。
数组:元素在内存中连续存储,即内存地址连续,所以查找数据效率高,但需要预先申请一块连续的内存空间,且运行期间数组大小无法动态增加减少。在插入、删除元素时效率比较低,伴随着后面元素位置的移动。
数组的应用场景:当数据量比较小,数据规模已知且需要经常访问元素时用数组更合适。
链表:动态申请 / 删除内存空间,对于元素的增删比较灵活,不需要预先知道数据规模,但无法随机查找。另外有序数组可以通过二分法来查找目标,但链表只能用顺序查找。
链表应用场景:对线性表规模难以估计,需要频繁进行插入删除操作。
2. 树结构
二叉树本质上是对数组和链表的折中。
2.1 二叉查找树
二叉查找树结合了有序数组的高效率查找和链表的高效率插入,时间复杂度为
O ( l o g 2 n ) O(log_2n) O(log2n)。适用于需要<font color="blue">频繁进行查询、插入、删除操作</font>的场景。但有可能退化成链表(此时只有左子树/右子树)此时查找和插入的最坏时间复杂度都是 O ( n ) O(n) O(n)。
2.2 平衡树
为了解决二叉查找树可能会退化为链表的问题,平衡树要求任意节点的左右两个子树的高度差不超过 1。平衡树的种类包括:AVL树,红黑树,SBT,Treap,伸展树。
2.2.1 平衡二叉搜索树(AVL 树)
AVL 树 = 平衡二叉搜索树(Self-balancing binary search tree),它要么是一颗空树,要么它的左右子树的高度差的绝对值不超过 1,且左右子树也满足以上性质。和红黑树相比,它是严格的平衡二叉树。在对 AVL 树进行插入或删除节点时,只要不满足平衡的性质就要通过旋转来保持平衡,比较耗时。
【应用场景】 适合插入删除操作都比较少、查找多的情况,AVL 保证了树深度为
O ( l o g 2 n ) O(log_2n) O(log2n)。Windows 内核使用 AVL 树保存一些离散的地址空间,原因大概是这些地址的访问次数多于插入删除的次数,且 AVL 树的高度低于红黑树。
2.2.2 红黑树
一种平衡的二叉搜索树,其在插入删除节点时旋转次数较少,故在插入删除多的情况下性能高于其他平衡二叉搜索树。
红黑树原理:通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束(红和黑),确保没有一条路径会比其他路径长2倍,因而是近似平衡的(一个节点的两棵子树高度不会相差二倍),是一棵弱平衡二叉树。所以相对于严格要求平衡的 AVL 树来说,它的旋转保持平衡次数较少。
【应用场景】
- C++ STL 包括set,multiset,map,multimap。C++关联容器,java容器中的TreeMap。- IO多路复用的epoll,内部用红黑树来维持我们想要监控的socket。以支持快速的插入和删除。- nginx中定时器是用红黑树来维持的,因为红黑树是有序的,每次从红黑树内部取出最小的定时器即可。- linux内核中用红黑树来管理进程的内存使用,进程的虚拟地址空间块都以虚拟地址为key值挂在这棵红黑树上。 ### 2.2.3 B 树和 B+ 树
B 树是一种平衡的多路查找树(查找路径不止两条)。在查找节点时,从根节点开始比较,若小于根节点则向左子树查找,否则向右子树查找。在多路分支中,找到合适的区间继续向下查找,直到查找到叶子节点,如果树结构中没有包含查找目标,则返回 null。
m 阶 B 树特点:
- 根结点至少有两个子女- 每个中间节点都包含 k-1 个元素和 k 个孩子,其中 m/2 <= k <= m- 每一个叶子节点都包含 k-1 个元素,其中 m/2 <= k <= m- 每个节点中的元素从小到大排列,节点当中 k-1 个元素正好是 k 个孩子包含的元素的值域分划- 所有的叶子结点都位于同一层 B+ 树是基于 B 树的一种变体,有着比 B 树更高的查询性能。B+ 树通常用于数据库和操作系统的文件系统中。B+ 树的特点是 能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入。
m 阶 B+ 树特点:
- 有 k 个子树的中间节点包含有 k 个元素(B 树中是 k-1 个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点- 所有的叶子结点中包含了全部元素的信息及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接- 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素
1. 每一个父节点的元素都出现在子节点中,且是子节点的最大(或最小)元素。1. 根节点的最大元素就是整个 B+ 树的最大元素,以后无论怎样插入删除,始终要保持最大元素在根节点中。1. 由于父节点元素都出现在子节点,因此所有叶子节点包含了全部元素信息。1. 每一个叶子节点都带有指向下一个节点的指针,形成了一个有序链表。 B+ 树与 B 树的区别: - B 树中无论中间节点还是叶子节点都带有卫星数据,而 B+ 树中只有叶子节点带有卫星数据,其余中间节点仅有索引,没有任何数据关联,所以同样大小的磁盘页可以容纳更多的节点元素,在数据量相同的情况下,B+ 树比 B 树更加 “矮胖”,查询时的 IO 次数也更少- B+ 树查询必须查找到叶子节点,而 B 树只要找到匹配元素即可,不管匹配元素处于中间节点还是叶子节点,因此 B 树查找性能不稳定(最好情况是只查到了根节点,最坏情况是查到了叶子节点),而 B+ 树的每一次查找都是稳定的- 范围查询时,B 树需要依靠繁琐的中序遍历,而 B+ 树的所有叶子节点形成有序链表,只需要在链表上做遍历即可 B+ 树的优点:
- 查询时的 IO 次数更少- 查找性能稳定- 便于范围查询 【应用场景】
B/B+树主要用在数据库或者大型索引数据中,是一种多路查找树。
B树,B+树:它们特点是一样的,是多路查找树,一般用于数据库系统中,为什么,因为它们分支多层数少呗(一般一个节点有成千上万个子节点,所以B树层数是非常少的,节点个数为N个高度为logN,但是这个log的底数不是我们常用的2了,而是一个节点的子节点个个数,底数非常大从而树高度很低),都知道磁盘IO是非常耗时的(和内存访问速度差千倍),而像大量数据存储在磁盘中所以我们要有效的减少磁盘IO次数避免磁盘频繁的查找。减少磁盘IO就意味着提升性能。
B+树是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据(也就是节点只保存key),只用来索引,数据都保存在叶子节点。是为文件系统而生的。
B树一般用在数据库等大型索引,主要原因就是层数少,重点—>减少避免磁盘IO
B+树适应文件系统,想下文件系统的样子,目录并不保存文件只有底层叶子节点保存数据。2.3 Trie树
单词查找树,主要处理字符串,字符串的相同前缀保存在相同的节点中。它的变种树非常多,比如前缀树(prefix tree),后缀树(suffix tree)。
它是不同字符串的相同前缀只保存一份,相对直接保存字符串肯定是节省空间的,但是它保存大量字符串时会很耗费内存(是内存)。
【应用场景】
前缀树(prefix tree): 2.字符串排序 4.自动匹配前缀显示后缀。
后缀树(suffix tree): 2.字符串s1在s2中出现的次数 4.最长回文串
详情还是见2.4 并查集(Union Find)
并查集是一种树型数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。凡是涉及到 元素的分组管理问题,都可以考虑使用并查集进行维护。
学习参考:算法学习笔记(1) : 并查集 - 知乎
2.5 哈夫曼树
哈夫曼树如何构成?
树的带权路径长度(WPL)
哈夫曼树是在叶子节点和权重确定的情况下,带权路径长度最小的二叉树,也称作最优二叉树。如何构建二叉树,才能保证其带权路径长度最小?
应该让权重小叶子节点的远离树根,让权重大的叶子节点靠近树根。计算方法:每个叶子节点的权重 x 根节点到该叶子节点的路径长度 之和
相同叶子节点构成的哈夫曼树可能不止一棵
构建哈夫曼树:
- 构建森林,把每个叶子节点都当作一棵独立的树(只有根节点的树);同时有一个辅助队列,队列中按照权值大小存储所有叶子节点。- 选择当前权值最小的两个节点,生成新的父节点,父节点的权值是这两个节点的权值之和- 从辅助队列删除上一步所选择的两个最小节点,将上一步生成的新的父节点加入队列,仍然保持队列的升序- 重复执行第二步操作- 最后队列中仅剩一个节点,说明整个森林已经合并为了哈夫曼树 ## 2.6 堆(Heap)
最大(小)堆是指在树中,若某结点有孩子结点,则该结点的值都不小于(大于)其孩子结点的值。在实现时,一般用 **数组** 来表示堆,
i i i 节点的父节点下标为 ( i − 1 ) / 2 (i-1) /2 (i−1)/2,左、右子节点的下标分别为 2 ∗ i + 1 2 * i + 1 2∗i+1 和 2 ∗ i + 2 2 * i+ 2 2∗i+2。
特点:堆是完全二叉树,但不一定是满二叉树。 堆节点的插入:新插入的元素放在数组最后,然后更新这棵树,更新过程类似于直接插入排序。 堆节点的删除:按定义堆中每次都删除第 0 个数据,即删除最小/最大的节点。实际操作是将堆中最后一个数据的值赋给根节点,然后再从根节点向下进行逐层调整,相当于将根节点下沉。 堆化数组:对于任意一棵二叉树的叶子节点,不需要调整次序,从最后一个非叶子节点开始调整。
3. 哈希表
可以实现
O ( 1 ) O(1) O(1) 级别的增、删、改、查效率。但哈希表基于数组实现,创建后难以扩展,
4. 栈和队列
栈是一种先进先出的结构,这个不用多说。在具体实现时,分为顺序栈和链栈,顺序栈用数组实现,链栈用链表实现。
顺序栈:元素存储位置连续,需要知道栈的最大长度才可以使用,无法避免溢出问题。当系统分配了数组存储空间,即使未使用到其他任务也不能使用该块空间,存储密度等于 1。顺序栈的
top
指针指向栈顶的空元素,top-1
指向栈顶元素。# 顺序栈实现 class Stack: def __init__(self): self.value = [] def push(self, item): # 压栈 self.value.append(item) def pop(self): # 出栈 if self.is_empty(): print('为空栈!') return return self.value.pop() def is_empty(self): # 判断空栈 return self.len() == 0 def len(self): # 当前栈大小 return len(self.value) def peek(self): # 查看栈顶元素 if self.is_empty(): print('为空栈!') return return self.value[-1]
链栈:元素存储不连续,动态申请地址,可以以非常小的初始内存空间开始。当某块内存未使用时,可以将内存返还给系统利用,存储密度 < 1。链栈的
top
指针相当于链表的head
指针,容易实现插入和删除操作。# 链栈实现 # 注意栈用链表实现时,栈顶应在链表头(而非链表尾),这样才能保证出栈正确 class ListNode: def __init__(self, item, next = None): self.item = item self.next = next class ListStack(): def __init__(self): self.top = None def push(self, item): # 入栈 self.top = ListNode(item, self.top) def pop(self): # 出栈 if self.top is None: print("空栈") return t = self.top self.top = self.top.next return t.item def is_empty(self): # 判断空栈 return self.top is None def top(self): # 查看栈顶元素 if self.top is None: print("空栈") return return self.top.item if __name__ == '__main__': st = ListStack() for i in range(1, 10, 2): st.push(i) while not st.is_empty(): print(st.pop())