默认标题_方形海报_2022-02-22+15_23_30.gif

1. 基础结构

数组

一段连续的物理内存区域,通过下标定位元素;

  • 查找时间复杂度:O(1);
  • 中间插入、移除数组中元素 时间复杂度 O(n);

链表

一种链式数据结构,由若干个节点组成,每个节点包含指向下一个节点的指针;
链表的物理存储方式是数据存储,访问方式是顺序访问。

查找时间复杂度:O(n);
中间插入、删除的时间复杂度:O(1);

汇总

常用的数据结构有很多;大多是以数组和链表为基础进行构建;因此数组和链表也可以叫做数据结构的物理结构;

image.png

2. 衍生结构

一种线性逻辑结构,可以用数组实现,也 可以用链表实现 ;先进后出原则;

队列

一种线性逻辑结构,可以用数组实现,也 可以用链表实现 ;先进先出原则;

哈希表

哈希表也叫散列表,是存储Key-Value映射的集合。对于某一个key, 哈希表可以在接近O(1) 的时间内进行读写操作。哈希表通过哈希函数实现 Key 和 数组下标的转化,通过开放地址法链地址法来解决哈希冲突;

写操作

先将key 利用哈希函数转化为 值,然后取模,定位到数组的位置,插入此处;

若此处已有值,
则采用开放地址法,在下一个没有被占用的地方插入;
或者采用链地址法,在此处的元素下,链接一个元素;

3. 树🌲

二叉树

树的一种特殊形式,顾名思义,这种树🌲每个节点最多有2个孩子节点。注:最多2个是指,可以是2个,也可以是1个,也可以没有;

结构如下所示:
image.png

满二叉树

一个二叉树的

  • 所有非叶子🍃节点都存在做孩子和右孩子;
  • 并且所有叶子🍃节点都在同一层级上;

那么这个🌲树就是满二叉树;

如下所示:
image.png

完全二叉树

对于有n个节点的二叉树,

  • 按层级顺序编号,则所有节点的编号为从1到n;
  • 若果这个树所有节点和同样深度的满二叉树的编号从1到n 的节点位置相同;

则这个二叉树为完全二叉树;

示例如下:
image.png

遍历

从宏观的角度来看,二叉树的遍历归结为两大类。

1) 深度优先遍历(前序遍历、中序遍历、后续遍历);

2)广度优先遍历(层序遍历);

注:深度优先遍历的条件是,以根节点为基准的,且先左后右,不这样的话,遍历的种类有 3! = 6种了;

前序遍历

即,根节点在首位;根节点、左节点、右节点;

image.png

中序遍历

即,根节点在中间;左节点、根节点、右节点;
image.png

后续遍历

即,根节点在后边;左节点、右节点、根节点;
image.png

遍历源码(java版)

TreeNode 定义

  1. /**
  2. *
  3. * 二叉树节点
  4. */
  5. @Data
  6. @NoArgsConstructor
  7. @AllArgsConstructor
  8. public class TreeNode {
  9. private Integer data;
  10. private TreeNode leftNode;
  11. private TreeNode rightNode;
  12. }

TreeTravese 遍历测试

  1. /**
  2. * @desc 二叉树遍历
  3. * @author liuph
  4. * @date 2022/02/22 17:03
  5. */
  6. public class TreeTraverse {
  7. /**
  8. *
  9. * <pre>
  10. * 创建一个二叉树,形式;
  11. *
  12. *
  13. * 1
  14. * / \
  15. * 2 3
  16. * / \ \
  17. * 4 5 6
  18. * </pre>
  19. */
  20. public TreeNode createTree(){
  21. TreeNode treeNode4 = new TreeNode(4, null, null);
  22. TreeNode treeNode5 = new TreeNode(5, null, null);
  23. TreeNode treeNode6 = new TreeNode(6, null, null);
  24. TreeNode treeNode2 = new TreeNode(2, treeNode4, treeNode5);
  25. TreeNode treeNode3 = new TreeNode(3, null, treeNode6);
  26. return new TreeNode(1, treeNode2, treeNode3);
  27. }
  28. public void firstTraverse(TreeNode node){
  29. System.out.println("当前遍历节点:" + node.getData());
  30. TreeNode leftNode = node.getLeftNode();
  31. TreeNode rightNode = node.getRightNode();
  32. if(Objects.nonNull(leftNode)){
  33. firstTraverse(leftNode);
  34. }
  35. if(Objects.nonNull(rightNode)){
  36. firstTraverse(rightNode);
  37. }
  38. }
  39. public void middleTraverse(TreeNode node){
  40. TreeNode leftNode = node.getLeftNode();
  41. TreeNode rightNode = node.getRightNode();
  42. if(Objects.nonNull(leftNode)){
  43. middleTraverse(leftNode);
  44. }
  45. System.out.println("当前遍历节点:" + node.getData());
  46. if(Objects.nonNull(rightNode)){
  47. middleTraverse(rightNode);
  48. }
  49. }
  50. public void backTraverse(TreeNode node){
  51. TreeNode leftNode = node.getLeftNode();
  52. TreeNode rightNode = node.getRightNode();
  53. if(Objects.nonNull(leftNode)){
  54. backTraverse(leftNode);
  55. }
  56. if(Objects.nonNull(rightNode)){
  57. backTraverse(rightNode);
  58. }
  59. System.out.println("当前遍历节点:" + node.getData());
  60. }
  61. @Test
  62. public void traverseTest(){
  63. System.out.println("----------前序遍历----------");
  64. TreeNode tree = createTree();
  65. firstTraverse(tree);
  66. System.out.println("----------中序遍历----------");
  67. middleTraverse(tree);
  68. System.out.println("----------后序遍历----------");
  69. backTraverse(tree);
  70. }
  71. }

二叉堆

一种完全二叉树,有两种:最大堆 和最小堆,可以用于创建优先队列(最大优先级队列、最小优先级队列);

最大堆特点:

  • 任何一个父节点,都不小于它任务子节点的值;

最小堆特点:

  • 任何一个父节点,都不大于它任务子节点的值;

物理存储:
一般使用数组进行存储,规则如下:
假设父节点为p, 左子节点 = 2p + 1, 右子节点 = 2p + 2;

堆排序

利用最大堆/最小堆的特性,就行排序;

示例:使用最大堆进行从小到大排序;

步骤:

  • 1)现将需要排序的数组转化成最大堆;
  • 2)取堆顶元素 与数组最后一位进行交换;
  • 3)将0-n-1 继续转化为最大堆,再次执行2)操作;
  • 4)重复2)、3),直到排序完成;

代码示例(java)

  1. public class HeapSortTest{
  2. // 向下调整
  3. public static void down(int [] a ,int start ,int end ){
  4. // 定义父节点、左孩子
  5. int p = start;
  6. int l = 2 * p + 1;
  7. while(l < end){
  8. if(a[l] < a[l+1]){ // 取最大的子节点
  9. l++;
  10. }
  11. if(a[p] < a[l]){ // 比父节点大,交换
  12. int temp = a[p];
  13. a[p] = a[l];
  14. a[l] = temp;
  15. }
  16. p = l;
  17. l = 2 * l + 1;
  18. }
  19. }
  20. // 升序排
  21. public static void sortAsc(int [] a){
  22. for(int i = a.length/2 - 1; i >= 0 ; i--){
  23. down(a, i, a.length - 1);
  24. }
  25. // 逐步取最大堆 堆顶,置换完成排序
  26. for(int i = a.length - 1; i > 0 ; i --){
  27. int temp = a[0];
  28. a[0] = a[i];
  29. a[i] = temp;
  30. down(a,0,i - 1);
  31. }
  32. }
  33. }

注:排序是为啥从 a.length - 2 开始呢,因为😄从队尾开始,保证每个元素都遍历,a.length = 2 * p + 2, p = a.length/2 - 1;

优先队列

此处优先队列基于二叉堆来实现,队列😄我们日常使用重点关注入队、出队,那么从这两个角度来说一下原理;

1)入队;

  • a. 默认也插入到数组尾部;
  • b. 做上浮操作(与 父节点 比较 ,大于父节点上浮);
  • c. 将父节点作为起始节点,再找父节点,上浮;
  • d. 重复b、c 完成入队;

2) 出队;

  • a. 弹出a[0] 元素;
  • b. 将最后一个元素,补齐到a[0];
  • c. a[0] 做下沉处理;

示例代码(java)

  1. public class MaxPriorityQueue {
  2. // 默认定义数组长度为1024
  3. private int [] a = new int [1024];
  4. // 定义当前存储的位置
  5. private int currentIndex = 0;
  6. // 添加元素
  7. public void add(int value){
  8. a[currentIndex]=value;
  9. if(currentIndex > 0){
  10. // 上浮操作
  11. up();
  12. }
  13. currentIndex++;
  14. }
  15. // 对数组最后一个存储的值进行上浮操作
  16. public void up(){
  17. // 计算父节点 l = 2p + 1, r = 2p + 2
  18. int p = currentIndex%2 == 0 ? (currentIndex-1)/2 : (currentIndex-2)/2;
  19. int currentTemp = currentIndex;
  20. while(p >= 0){
  21. if(a[p] < a[currentTemp]){ // 父节点 < 当前节点 ,交换
  22. int temp = a[p];
  23. a[p] = a[currentTemp];
  24. a[currentTemp] = temp;
  25. // 节点向上调整
  26. currentTemp = p;
  27. p = p%2 == 0 ? p/2 - 2 : p/2 -1;
  28. }else{
  29. break;
  30. }
  31. }
  32. }
  33. // 弹出元素,尾节点替换a[0],向下调整
  34. public int pop(){
  35. int value = a[0];
  36. a[0] = a[currentIndex - 1];
  37. a[currentIndex - 1] = 0;
  38. // 向下调整
  39. down();
  40. currentIndex--;
  41. return value;
  42. }
  43. // a[0] 向下调整
  44. public void down(){
  45. int p = 0;
  46. int l = 1;
  47. while(l < currentIndex){
  48. if(a[l] < a[l+1]){ // 取最大的子节点
  49. l++;
  50. }
  51. if(a[p] < a[l]){ // 父 < 最大子 ,替换
  52. int temp = a[p];
  53. a[p] = a[l];
  54. a[l] = temp;
  55. // 向下调整
  56. p = l;
  57. l = 2*l + 1;
  58. }else{
  59. break;
  60. }
  61. }
  62. }
  63. public void print(){
  64. for (int i = 0; i < currentIndex; i++) {
  65. System.out.print(a[i] + ",");
  66. }
  67. }
  68. @Test
  69. public void test(){
  70. MaxPriorityQueue priorityQueue = new MaxPriorityQueue();
  71. priorityQueue.add(6);
  72. priorityQueue.add(7);
  73. priorityQueue.add(9);
  74. priorityQueue.add(5);
  75. priorityQueue.add(4);
  76. System.out.print("最大堆:");
  77. priorityQueue.print();
  78. System.out.println();
  79. priorityQueue.pop();
  80. System.out.print("pop后最大堆:");
  81. priorityQueue.print();
  82. }
  83. }

红黑树

用途

jdk8的hashmap 当链表长度>8 是,自动转化为红黑树;
TreeMap、TreeSet 都使用红黑树作为底层数据结构;

为了更好的介绍红黑树,我们从 (BST)查找树、(AVL)平衡树、再到(RBT)红黑树;
三者的关系如下:
平衡树、红黑树 均属于 排序二叉树;
image.png

查找树特点:

1)左子树上的所有节点的值均小于或者等于 它的 根节点的值;
2)右子树上所有的节点的值均大于或者等于 它的 根节点的值;
3) 左、右树叶分别为二叉排序树;

完美的情况下是这样的;

image.png
由于排序二叉树的特性,不断的插入可能演变成为 链表结构;之间复杂度一下 成了O(n);

image.png

平衡树特点:

  • 在查找树的基础上,新增如下特点:
  • 每个节点的左、右字数的高度差绝对值最多为1;

平衡功能的实现:
如下图,新增1会导致 高度差 > 1 , 此时平衡树会进行平衡,AVL 的调整过程称为:左旋和右旋;

image.png

旋转之前,首先确定旋转支点(pivot):这个旋转点就是失去平衡的这部分树,也是自平衡后的根节点

左旋就是逆时针转,右旋是顺时针转
导致失衡的场景有四个:
左左失衡;
右右失衡;
左右失衡;
右左失衡;

删除元素,也会导致AVL失衡,需要再平衡,但是原理和插入元素是类似的。

场景一:左左失衡
image.png

7为支点,即旋转后的 root 节点,根据7进行旋转,9向下右旋,8作为9点左节点;

右右失衡 与之对应,不予介绍;

场景二: 左右失衡

即:插入的元素在左侧不平衡元素的右侧

image.png

先以7为支点,6向左下旋转,7上接9; 然后 就是左左结构了,以7为支点,9向右下旋转,8接9;

右左与 左右对应,不予介绍;

平衡树解决了 查找树可能转化成链表的为题; 那么为啥还会有红黑树呢?
就是因为平衡树的绝对平衡,新增、插入、删除的时候很容易就出现需要需要平衡的情况;导致新增、插入、删除效率低;

红黑树特点:

1) 所有节点非黑即红;
2)根节点、叶子节点均为黑色;
3)根几点 到叶子节点 中途 遇到的 黑点 ,所有线路个数一致;
4) 不能出现两个红节点相连的情况;

红黑树的平衡方式:变色、左旋、右旋

参见

  • 小灰学算法

数据结构 - 图15