1. 树的基本语术

image.png

  • Degree:度,结点所拥有的⼦树个数称为结点的度

  • Level:层次,根为第0层,根的孩⼦为第1层。若某结点在第i层,那么其⼦树的根就在第i + 1

  • Height of node:结点的⾼度,该结点到叶⼦结点,最⻓路径的边数

  • Height of tree:树的⾼度 = 根结点的⾼度

  • Depth of node:结点的深度,从根结点到该结点的路径中,边的数量

  • Ancestor: 祖先,结点的祖先(Ancestor)是从根(Root)到该结点所经分⽀(Branch)上的所有结点

  • 路径:在⼀棵树中,⼀个结点到另⼀个结点之间的通路,称为路径

2. 树的种类

  • ⼆叉树

    • 霍夫曼树(最优⼆叉树)
  • ⼆叉搜索树

  • 平衡搜索树

    • ⾃平衡⼆叉搜索树

      • AVL树

      • 红⿊树(2-3树)

  • 多路搜索树

    • 多路平衡搜索树

      • B树

      • B+树

    • 2-3树

    • 2-3-4树

2.1 ⼆叉搜索树

⼆叉查找树(BST:Binary Search Tree)是⼀种特殊的⼆叉树,它改善了⼆叉树结点查找的效率
image.png

  • 对于任意⼀个结点n

  • 其左⼦树(left subtree)下的每个后代结点(descendant node)的值都⼩于结点n的值

  • 其右⼦树(right subtree)下的每个后代结点的值都⼤于结点n的值

  • 其左⼦树和右⼦树也是⼆叉搜索树

  • 时间复杂度:⼀般情况插⼊、搜索和删除操作O(logn),最坏情况O(n)

  • 空间复杂度:O(n)

  • 遍历类型:

    • 前序遍历:Parent-LeftChild-RightChild

    • 中序遍历:LeftChild-Parent-RightChild

    • 后序遍历:LeftChild -RightChild-Parent

2.2 ⾃平衡⼆叉搜索树

2.2.1 AVL树

image.png

  • 时间复杂度:插⼊、搜索和删除操作O(logn)

  • 空间复杂度:O(n)

    • 平衡因⼦:⼆叉树中每个结点的左⼦树和右⼦树的⾼度差

      • 如果结点仅有⼀个⼦结点,则⽆⼦结点侧的⾼度为-1

      • AVL树中每个结点的平衡因⼦应该是+10-1

    • 对于任意⼀个结点n

      • 平衡因⼦的绝对值不超过1

      • 结点n的左⼦树的⾼度与右⼦树的⾼度的差⾄多是1

    • 在每次插⼊或删除后,如果任何结点的平衡因⼦不遵循 AVL 平衡属性

      • 左旋

      • 右旋

      • 左右旋

      • 右左旋

2.2.1 红⿊树

image.png

  • 时间复杂度:插⼊、搜索和删除操作O(logn)

  • 空间复杂度:O(n)

  • 红⿊树还添加了⼀种特殊类型的结点 ,称为NIL结点。NIL结点将做为红⿊树的伪叶⼦结点出现

  • 对于任意⼀个结点n

    • 结点的颜⾊只能是红⾊或者⿊⾊

    • 根结点是⿊⾊的

    • 从根到任何叶结点的每条路径必须具有相同数量的⿊⾊结点

    • 没有两个红⾊结点可以相邻,即⼀个红⾊结点不能是另⼀个红⾊结点的⽗结点或⼦结点

    • NIL结点的颜⾊是⿊⾊

    • 如果结点的颜⾊是红⾊,则其⼦结点均为⿊⾊

  • 在每次插⼊或删除后,如果任何结点的平衡因⼦不遵循平衡属性

    • 左旋

    • 右旋

2.2.3 AVL树 VS 红⿊树

  • 红⿊树是不符合AVL树的平衡条件的,即每个结点的左⼦树和右⼦树的⾼度最多差1的⼆叉查找树

  • 红⿊树是⽤⾮严格的平衡来换取增删结点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决

  • AVL是严格平衡树,因此在增加或者删除结点的时候,根据不同情况,旋转的次数⽐红⿊树要多。所以红⿊树的插⼊效率更⾼

  • AVL在查询频繁的系统⾥⽐红⿊树更⾼效,原因是AVL树的平均⾼度⽐红⿊树更⼩

3. 堆结构

堆是⾼级数据结构,主要⽤来排序和实现优先级队列

3.1 关键属性

堆本质上是⼆叉树,但是必须满⾜两个条件:

  • 是完全⼆叉树

    • left child = 2 * parent + 1

    • right child = 2 * parent + 2

    • parent = floor((child - 1) / 2)

  • 结点必须根据顺序存储

3.2 ⼤顶堆 & ⼩顶堆

image.png

  • ⼤顶堆:⽗结点的值⼤于等于⼦结点的值。所以根结点始终等于当前堆值最⼤的元素

  • ⼩顶堆:⽗结点的值⼩于等于⼦结点的值。所以根结点始终等于当前堆值最⼩的元素

3.3 完全⼆叉树 & 满⼆叉树

image.png

  • 满⼆叉树

    • ⼆叉树除了叶结点外所有结点都有两个⼦结点

    • ⼀个深度为k(>=-1)且有2 ^ (k + 1) - 1个结点的⼆叉树称为完美⼆叉树

  • 完全⼆叉树

    • ⼆叉树中除去最后⼀层结点为满⼆叉树,且最后⼀层的结点依次从左到右分布

    • 完全⼆叉树从根结点到倒数第⼆层满⾜完美⼆叉树,最后⼀层可以不完全填充,其叶⼦结点都靠左对⻬

4. 堆排序

  • 堆排序算法的最坏性能为:O(N*log(N))

  • 堆排序算法是所有排序算法中最坏性能中表现最好的排序算法

  • 由于堆是完全⼆叉树,可以统计每⼀层的结点:

    • 0层包含根结点

    • 1层包含根结点的⼦结点

    • L层包含L-1层结点的⼦结点

    • 如果在当前完全⼆叉树在L层只有⼀个结点,整棵树⼀共有N个结点:

      • 2 ^ 0 + 2 ^ 1 + … + 2 ^ (L - 1) + 1 = 2 ^ L <= N <= 2 ^ 0 + 2 ^ 1 + … + 2 ^ L = 2 ^ (L + 1) - 1

      • L <= log(N) < L + 1

      • 可以得出结论,从根到任何叶⼦的最⻓分⽀最多包含log(N)个结点

      • 任何仅访问单个分⽀上的结点的算法复杂度都是:O(log(N))

4.1 添加元素

向堆⾥添加F, E, D, C, B, A,并且F > E > D > C > B > A ,实现⼩顶堆
image.png

在下⼀个可能的位置添加⼀个具有新值的结点,将树保持为完全⼆叉树

将新结点与其⽗结点进⾏⽐较,当新结点⼩于⽗结点时,交换结点

重复上⼀步,直到新结点⼩于⽗结点或成为根结点

4.1.1 思路分析

添加元素的顺序为:F, E, D, C, B, A

添加第1个元素F,直接作为根结点
image.png

添加第2个元素E,与父结点F进行比较,如果<= F,则交换
image.png

添加第3个元素D,与父结点E进行比较,如果<= E,则交换
image.png

添加第4个元素C,与父结点F进行比较,如果<= F,则交换
image.png

此时结点C依然存在父结点D,继续比较。如果<= D,则再交换
image.png

按照上述逻辑,循环往复,将所有结点插入,形成小顶堆

4.1.2 代码实现

基本函数:

  1. enum HeapType : Int {
  2. case Big = 1
  3. case Small = 2
  4. };
  5. class Heap<Element: Comparable> {
  6. fileprivate var _arr : [Element];
  7. fileprivate var _heapType : HeapType;
  8. init(heapType: HeapType) {
  9. _heapType = heapType;
  10. _arr = [Element]();
  11. }
  12. // 交换函数
  13. fileprivate func swap(i : Int, j : Int) {
  14. let tmp = _arr[i];
  15. _arr[i] = _arr[j];
  16. _arr[j] = tmp;
  17. }
  18. // 打印元素
  19. func traverse() {
  20. var str = "";
  21. for i in 0..<_arr.count {
  22. str += "\(_arr[i]), ";
  23. }
  24. print("\(str)");
  25. }
  26. }

辅助方法:

  1. class Heap<Element: Comparable> {
  2. // 返回父结点索引值
  3. fileprivate func getParentIndex(index: Int) -> Int{
  4. // 根结点没有父结点,返回-1
  5. if(index == 0){
  6. return -1;
  7. }
  8. return (index - 1) / 2;
  9. }
  10. // 返回左孩子索引值
  11. fileprivate func getLeftIndex(index: Int) -> Int{
  12. return index * 2 + 1;
  13. }
  14. // 返回右孩子索引值
  15. fileprivate func getRightIndex(index: Int) -> Int{
  16. return index * 2 + 2;
  17. }
  18. // 根据【大/小顶堆】的特性,实现比较方法
  19. fileprivate func compare(curr: Element, target: Element) -> Bool {
  20. if(_heapType == HeapType.Big){
  21. return curr >= target;
  22. }
  23. return curr <= target;
  24. }
  25. }

添加元素:

  1. class Heap<Element: Comparable> {
  2. // 添加元素
  3. func add(element: Element) {
  4. // 添加新元素
  5. _arr.append(element);
  6. // 获取最后一个元素的索引值
  7. let index = _arr.count - 1;
  8. // 当前结点与⽗结点⽐较,将其调整至符合堆的特性
  9. parentSwap(index: index);
  10. }
  11. // 当前结点与⽗结点⽐较,将其调整至符合堆的特性
  12. func parentSwap(index: Int) {
  13. // 传入当前结点
  14. var currIndex = index;
  15. // 获取该结点的父节点的索引值
  16. var parentIndex = getParentIndex(index: currIndex);
  17. // 如果父节点为-1,表示当前结点为根结点,停止遍历
  18. while(parentIndex != -1){
  19. // 比较当前结点与父结点,如果父节点更符合堆的特性,不需要交换,退出循环
  20. if(!compare(curr: _arr[currIndex], target: _arr[parentIndex])){
  21. break;
  22. }
  23. // 否则,当前结点更符合堆的特性,与父节点交换
  24. swap(i: currIndex, j: parentIndex);
  25. // 当前结点赋值为父节点
  26. currIndex = parentIndex;
  27. // 寻找新的父节点,准备下一轮遍历
  28. parentIndex = getParentIndex(index: currIndex);
  29. }
  30. }
  31. }

4.2 删除元素

从堆⾥删除A, B, C, D, E, F

  • 待删除结点与最后⼀个结点交换,先临时保存待删除结点,然后将其删除

  • 比较待删除结值与最后⼀个结点

    • 如果待删除结点大于最后⼀个结点,比较交换后结点与其原先父节点,如果有必要,交换

    • 如果待删除结点小于最后⼀个结点,比较交换后结点与其原先子节点,如果有必要,交换

  • 重复上⼀步,直到⽗结点⼩于其原先⼦结点

4.2.1 思路分析

删除元素的顺序为:A, B, C, D, E, F

删除第1个元素A,将待删除元素与最后一个元素交换,然后将元素A删除
image.png

元素E与它的左、右孩子对比,其中B在这三个结点中最符合小顶堆特性,故此EB交换
image.png

删除第2个元素B,将待删除元素与最后一个元素交换,然后将元素B删除
image.png

元素D与它的左、右孩子对比,其中C在这三个结点中最符合小顶堆特性,故此DC交换
image.png

按照上述逻辑,循环往复,可将所有结点删除

4.2.2 堆化

  • ⽤O(n)的时间把⼀个乱序的数组变成⼀个堆,这一过程称之为堆化

  • 将⼀个数组当成⼀个完全⼆叉树,然后从最后⼀个⾮叶⼦节点开始,逐个对⾮叶⼦节点进⾏下沉操作

  • 当删除元素时,如果待删除结点小于最后⼀个结点,则比较交换后结点与其原先子节点,此时进行的就是堆化处理

4.2.3 代码实现

堆化:

  1. class Heap<Element: Comparable> {
  2. // 堆化 - ⾮叶⼦节点与左、右孩子比较,逐个对⾮叶⼦节点进⾏下沉操作,将其调整至符合堆的特性
  3. fileprivate func heapify(index: Int) {
  4. // 记录当前索引值
  5. var currIndex = index;
  6. // 获取左孩子的索引值
  7. var leftIndex = getLeftIndex(index: currIndex);
  8. // 判断左孩子如果越界,证明当前结点为叶子结点,停止遍历
  9. while(leftIndex < _arr.count){
  10. // 将交换目标的索引值,赋值为当前索引值
  11. var targetIndex = currIndex;
  12. // 如果左孩子相比当前结点更符合堆的特性
  13. if(compare(curr: _arr[leftIndex], target: _arr[targetIndex])){
  14. // 将交换目标的索引值,赋值为左孩子的索引值
  15. targetIndex = leftIndex;
  16. }
  17. // 获取右孩子的索引值
  18. let rightIndex = getRightIndex(index: currIndex);
  19. // 如果右孩子的索引值没有越界
  20. // 并且右孩子相比当前结点更符合堆的特性
  21. if(rightIndex < _arr.count && compare(curr: _arr[rightIndex], target: _arr[targetIndex])){
  22. // 将交换目标的索引值,赋值为右孩子的索引值
  23. targetIndex = rightIndex;
  24. }
  25. // 如果交换目标的索引值为当前索引值,表示当前结构符合堆的特性
  26. if(targetIndex == currIndex){
  27. // 退出循环
  28. break;
  29. }
  30. // 否则,交换当前元素与目标元素
  31. swap(i: currIndex, j: targetIndex);
  32. // 将当前索引值赋值为目标元素的索引值
  33. currIndex = targetIndex;
  34. // 寻找新的左孩子,准备下一轮循环
  35. leftIndex = getLeftIndex(index: currIndex);
  36. }
  37. }
  38. }

删除元素:

  1. class Heap<Element: Comparable> {
  2. // 删除指定元素
  3. func remove(element: Element) {
  4. // 寻找该元素在数组中首次出现的索引值
  5. let index = _arr.firstIndex(of: element);
  6. // 未找到,不作处理
  7. if(index == nil){
  8. return;
  9. }
  10. // 按照索引位置删除元素
  11. remove(index: index!);
  12. }
  13. // 删除索引位置的元素
  14. func remove(index: Int) {
  15. // 如果索引越界,不作处理
  16. if(index >= _arr.count){
  17. return;
  18. }
  19. // 如果当前数组只有一个元素,或者删除的是最后一个元素
  20. if(_arr.count == 1 || index == _arr.count - 1){
  21. // 直接删除即可
  22. _arr.remove(at: index);
  23. return;
  24. }
  25. // 获取最后一个元素的索引值
  26. let lastIndex = _arr.count - 1;
  27. // 获取待删除元素
  28. let curr = _arr[index];
  29. // 获取最后一个元素
  30. let last = _arr[lastIndex];
  31. // 将待删除元素与最后一个元素交换
  32. swap(i: index, j: lastIndex);
  33. // 将待删除元素删除
  34. _arr.remove(at: lastIndex);
  35. // 如果删除结点相比最后一个结点不符合堆的特性
  36. if(!compare(curr: curr, target: last)){
  37. // 使用交换后的新结点与原父结点⽐较,将其调整至符合堆的特性
  38. parentSwap(index: index)
  39. return;
  40. }
  41. // 否则,删除结点相比最后一个结点更符合堆的特性,进行堆化处理
  42. heapify(index: index);
  43. }
  44. }

总结

树的基本语术:

  • Degree:度,结点所拥有的⼦树个数称为结点的度

  • Level:层次,根为第0层,根的孩⼦为第1层。若某结点在第i层,那么其⼦树的根就在第i + 1

  • Height of node:结点的⾼度,该结点到叶⼦结点,最⻓路径的边数

  • Height of tree:树的⾼度 = 根结点的⾼度

  • Depth of node:结点的深度,从根结点到该结点的路径中,边的数量

  • Ancestor: 祖先,结点的祖先(Ancestor)是从根(Root)到该结点所经分⽀(Branch)上的所有结点

  • 路径:在⼀棵树中,⼀个结点到另⼀个结点之间的通路,称为路径

树的种类:

  • ⼆叉树

    • 霍夫曼树(最优⼆叉树)
  • ⼆叉搜索树

  • 平衡搜索树

    • ⾃平衡⼆叉搜索树

      • AVL树

      • 红⿊树(2-3树)

  • 多路搜索树

    • 多路平衡搜索树

      • B树

      • B+树

    • 2-3树

    • 2-3-4树

堆结构:

  • 堆本质上是⼆叉树,但是必须满⾜两个条件:

    • 是完全⼆叉树

      • left child = 2 * parent + 1

      • right child = 2 * parent + 2

      • parent = floor((child - 1) / 2)

    • 结点必须根据顺序存储

  • ⼤顶堆 & ⼩顶堆

    • ⼤顶堆:⽗结点的值⼤于等于⼦结点的值。所以根结点始终等于当前堆值最⼤的元素

    • ⼩顶堆:⽗结点的值⼩于等于⼦结点的值。所以根结点始终等于当前堆值最⼩的元素

  • 完全⼆叉树 & 满⼆叉树

    • 满⼆叉树

      • ⼆叉树除了叶结点外所有结点都有两个⼦结点

      • ⼀个深度为k(>=-1)且有2 ^ (k + 1) - 1个结点的⼆叉树称为完美⼆叉树

    • 完全⼆叉树

      • ⼆叉树中除去最后⼀层结点为满⼆叉树,且最后⼀层的结点依次从左到右分布

      • 完全⼆叉树从根结点到倒数第⼆层满⾜完美⼆叉树,最后⼀层可以不完全填充,其叶⼦结点都靠左对⻬

堆排序:

  • 堆排序算法的最坏性能为:O(N*log(N))

  • 堆排序算法是所有排序算法中最坏性能中表现最好的排序算法

  • 任何仅访问单个分⽀上的结点的算法复杂度都是:O(log(N))

添加元素:

  • 在下⼀个可能的位置添加⼀个具有新值的结点,将树保持为完全⼆叉树

  • 将新结点与其⽗结点进⾏⽐较,当新结点⼩于⽗结点时,交换结点

  • 重复上⼀步,直到新结点⼩于⽗结点或成为根结点

删除元素:

  • 待删除结点与最后⼀个结点交换,先临时保存待删除结点,然后将其删除

  • 比较待删除结值与最后⼀个结点

    • 如果待删除结点大于最后⼀个结点,比较交换后结点与其原先父节点,如果有必要,交换

    • 如果待删除结点小于最后⼀个结点,比较交换后结点与其原先子节点,如果有必要,交换

  • 重复上⼀步,直到⽗结点⼩于其原先⼦结点

堆化:

  • ⽤O(n)的时间把⼀个乱序的数组变成⼀个堆,这一过程称之为堆化

  • 将⼀个数组当成⼀个完全⼆叉树,然后从最后⼀个⾮叶⼦节点开始,逐个对⾮叶⼦节点进⾏下沉操作

  • 当删除元素时,如果待删除结点小于最后⼀个结点,则比较交换后结点与其原先子节点,此时进行的就是堆化处理