前言:

一 线性表

线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是由n个具有相同特性数据元素组成的有限序列。

1.1 相关概念

前驱元素:若A元素在B元素的前面,则称A为B的前驱元素
后继元素:若B元素在A元素的后面,则称B为A的后继元素
线性表的特征:数据元素之间具有一种“一对一”的逻辑关系。
1. 第一个数据元素没有前驱,这个数据元素被称为头结点。
2. 最后一个数据元素没有后继,这个数据元素被称为尾结点。
3. 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。

如果把线性表用数学语言来定义,则表示为(a1,…ai-1,ai,ai+1,…an),ai-1领先于ai,ai领先于ai+1,称ai-1是ai的前驱元素,ai+1是ai的后继元素。
image.png
线性表的分类:
线性表中的数据物理存储方式可分为顺序存储、链式存储,即线性表分为顺序表和链表。

二 顺序表

顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中逻辑结构的数据与物理结构的数据一致(相邻的两个元素逻辑和物理存储单元位置一致)。即通过数据元素物理存储相邻关系来反映数据元素逻辑上相邻关系。

2.1 代码实现

关键代码:

  1. add添加元素
  2. add重载添加元素
  3. remove 移除元素
  4. ensureCapacity 扩容以及缩容 ```java package com.ycc.data.structure.line;

import java.util.Iterator;

/**

  • 顺序表 *
  • @author liaozx
  • @description
  • @create 2020-11-23 22:12 / public class MyArrayList implements Iterable { /*

    • 用来保存此队列中内容的数组 / private Object[] elementData; /*
    • 保存当前为第几个元素的指标 / private int current; /*
    • 表示数组大小的容量指标 */ private int capacity;

      /**

    • 初始化线性表,并且声明保存内容的数组大小 *
    • @param initSize */ public MyArrayList(int initSize) { if (initSize < 0) {

      1. throw new RuntimeException("initalSize必须大于0:" + initSize);

      } else {

      1. //初始化数组
      2. this.elementData = new Object[initSize];
      3. this.current = 0;
      4. this.capacity = initSize;

      } }

      /**

    • 如果初始化时,未声明大小,则默认为10 */ public MyArrayList() { this(10); }

      /**

    • 添加元素的方法 添加前,先确认是否已经满了 *
    • @param e
    • @return */ public void add(E e) { // 确认容量 ensureCapacity(); this.elementData[current++] = e; }

      /**

    • 在指定下标位置处插入数据e *
    • @param index 下标
    • @param e 需要插入的数据
    • @return */ public void add(int index, E e) { validateIndex(index); ensureCapacity(); //先把index索引处的元素及其后面的元素依次向后移动一位 for (int i = current; i > index; i—) {

      1. elementData[i] = elementData[i - 1];

      } //再把t元素放到i索引处即可 elementData[index] = e; //元素个数+1 current++; }

      /**

    • 删除指定下标出的数据 *
    • @param index
    • @return */ public E remove(int index) { validateIndex(index); Object temp = elementData[index]; //把index的后面元素向前移动一个 for (int i = index; i < current - 1; i++) {
      1. elementData[i] = elementData[i + 1];
      } current—; ensureCapacity(); return (E) temp; }
  1. /**
  2. * 确认系统当前容量是否满足需要,如果满足,则不执行操作 如果不满足,增加容量
  3. */
  4. private void ensureCapacity() {
  5. boolean isResize = false;
  6. //扩容 默认扩展2倍
  7. if (current == capacity) {
  8. capacity = capacity * 2;
  9. isResize = true;
  10. }
  11. //缩容 当数据量小于 容量的1/4 则需要缩容成1/2
  12. if (current < capacity / 4) {
  13. capacity = capacity / 2;
  14. isResize = true;
  15. }
  16. if (isResize) {
  17. Object[] newData = new Object[capacity];
  18. for (int i = 0; i < current; i++) {
  19. newData[i] = this.elementData[i];
  20. }
  21. this.elementData = newData;
  22. }
  23. }
  24. /**
  25. * 得到指定下标的数据
  26. *
  27. * @param index
  28. * @return
  29. */
  30. public E get(int index) {
  31. validateIndex(index);
  32. return (E) this.elementData[index];
  33. }
  34. /**
  35. * 返回当前队列大小
  36. *
  37. * @return
  38. */
  39. public int size() {
  40. return this.current;
  41. }
  42. /**
  43. * 验证当前下标是否合法,如果不合法,抛出运行时异常
  44. *
  45. * @param index 下标
  46. */
  47. private void validateIndex(int index) {
  48. if (index < 0 || index > current) {
  49. throw new RuntimeException("数组Index错误:" + index + "范围是:0-" + current);
  50. }
  51. }
  52. @Override
  53. public Iterator iterator() {
  54. return new MyArrayListIterator();
  55. }
  56. //实现迭代器
  57. private class MyArrayListIterator implements Iterator {
  58. private int curr;
  59. public MyArrayListIterator() {
  60. curr = 0;
  61. }
  62. @Override
  63. public boolean hasNext() {
  64. return curr < current;
  65. }
  66. @Override
  67. public Object next() {
  68. return elementData[curr++];
  69. }
  70. }

}

  1. 测试代码
  2. ```java
  3. package com.ycc.data.structure.test;
  4. import com.alibaba.fastjson.JSONObject;
  5. import com.ycc.data.structure.line.MyArrayList;
  6. /**
  7. * @author liaozx
  8. * @date 2020-11-23
  9. */
  10. public class MyArrayListTest {
  11. public static void main(String[] args) {
  12. MyArrayList<String> myArrayList = new MyArrayList<>(3);
  13. myArrayList.add("张三");
  14. myArrayList.add("李四");
  15. myArrayList.add("王五");
  16. myArrayList.add(3, "赵六");
  17. System.out.println(JSONObject.toJSONString(myArrayList));
  18. for (String str : myArrayList) {
  19. System.out.println(str);
  20. }
  21. }
  22. }

2.2 时间复杂度分析

  1. 查询某个元素: get(i): 只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1);
  2. add(int i,T t):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n);
  3. remove(int i):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n);

PS: 扩容导致性能下降
顺序表的底层由数组实现,且数组长度是固定的,所以在操作过程中涉及到容器扩容操作。这样会导致顺序表在使用过程中时间复杂度不是线性的,因为它在某些时刻需要扩容,这时耗时会突增,尤其是元素越多,问题越明显。

三 链表

链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能表示元素的逻辑顺序。数据元素的逻辑顺序是通过链表中指针连接实现,即每个结点都有一个指针(next)指向下一个节点。

3.1 单链表代码实现

单向链表是链表中的一种,它由多个结点组成,且每个结点都由一个数据域和一个指针域组成,数据域存储数据,指针域指向其后继结点。链表头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。
image.png
关键代码:

  1. add 新增元素
  2. add 重载新元素
  3. remove移除元素 ```java package com.ycc.data.structure.line;

import java.util.Iterator;

/**

  • 单向链表 *
  • @author liaozx
  • @description
  • @create 2020-11-23 23:12 / public class MyLinedList implements Iterable { /*

    • 头结点 */ private final Node head;

      /**

    • 表示数组大小的指标 */ private int current;

      public MyLinedList() { //初始化头结点 this.head = new Node(null, null); //初始化元素个数 this.current = 0; }

      /**

    • 在链表的末尾插入节点 *
    • @param e */ public void add(E e) { //找到当前最后一个结点 Node node = head; while (node.next != null) {

      1. node = node.next;

      } // 根据需要添加的内容,封装为结点 Node newNode = new Node<>(e, null); node.next = newNode; // 当前大小自增加1 current++; }

      /**

    • 指定index位置,新增一个对象 *
    • @param index
    • @param e */ public void add(int index, E e) { //获取index节点 Node currentNode = getNode(index); //创建新结点,并且newNode.next=indexNode.next;即断开链表 Node newNode = new Node(e, currentNode.next); //然后插入链表节点 currentNode.next = newNode; //元素的个数+1 current++; }

      /**

    • 移除一个元素 *
    • @param index
    • @return */ public E remove(int index) { //找到index位置的前一个节点 Node preNode = getNode(index); //找到index结点 Node currentNode = preNode.next; //找到index位置的下一个节点 Node nextNode = currentNode.next; //前一个结点指向下一个结点 preNode.next = nextNode; //元素个数-1 current—; return (E) currentNode.item; }

      /**

    • 遍历当前链表,取得当前索引对应的元素 *
    • @return */ private Node getNode(int index) { // 先判断索引正确性 if (index > current || index < 0) {

      1. throw new RuntimeException("索引值有错:" + index + ",其范围是0-" + current);

      } Node currentNode = head; int count = 0; while (count != index) {

      1. currentNode = currentNode.next;
      2. count++;

      } return currentNode; }

      /**

    • 根据索引,取得该索引下的数据 *
    • @param index
    • @return */ public E get(int index) { // 先判断索引正确性 if (index >= current || index < 0) {

      1. throw new RuntimeException("索引值有错:" + index);

      } //因为头节点,不存数据,即head.next 为第一个节点 Node tem = head.next; int count = 0; while (count != index) {

      1. tem = tem.next;
      2. count++;

      } return tem.item; }

      public int size() { return current; }

      @Override public Iterator iterator() { return new MyLinedListIterator(); }

/**

  1. * 用来存放数据的结点型内部类
  2. */
  3. private class Node<E> {
  4. private final E item;// 结点中存放的数据
  5. private Node<E> next;// 用来指向该结点的下一个结点
  6. public Node(E item, Node next) {
  7. this.item = item;
  8. this.next = next;
  9. }
  10. }
  11. private class MyLinedListIterator implements Iterator {
  12. private Node node;
  13. public MyLinedListIterator() {
  14. this.node = head;
  15. }
  16. @Override
  17. public boolean hasNext() {
  18. return node.next != null;
  19. }
  20. @Override
  21. public Object next() {
  22. node = node.next;
  23. return node.item;
  24. }
  25. }

}

  1. 测试代码
  2. ```java
  3. package com.ycc.data.structure.test;
  4. import com.alibaba.fastjson.JSONObject;
  5. import com.ycc.data.structure.line.MyLinedList;
  6. /**
  7. * @author liaozx
  8. * @date 2020/11/23
  9. */
  10. public class MyLinedListTest {
  11. public static void main(String[] args) {
  12. //创建顺序表对象
  13. MyLinedList<String> sl = new MyLinedList<>();
  14. //测试插入
  15. sl.add("烟雨楼0");
  16. sl.add("烟雨楼1");
  17. sl.add("烟雨楼2");
  18. sl.add(2, "烟雨楼3");
  19. System.out.println(JSONObject.toJSONString(sl));
  20. System.out.println(JSONObject.toJSONString(sl.get(0)));
  21. for (String str: sl) {
  22. System.out.println(str);
  23. }
  24. }
  25. }

3.2 双向链表代码实现

双向链表也叫双向表,是链表中的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域存储数据,其中一个指针域指向其后继结点,另一个指针域指向前驱结点。链表头结点的数据域不存数据,指向前驱结点的指针值为null,指向后继结点的指针域指向第一个真正存储数据的结点。
image.png

  1. package com.ycc.data.structure.line;
  2. import java.util.Iterator;
  3. /**
  4. * 双向链表
  5. *
  6. * @author liaozx
  7. * @date 2020/11/23
  8. */
  9. public class MyBothWayLinedList<E> implements Iterable<E> {
  10. /**
  11. * 头结点
  12. */
  13. private final Node<E> head;
  14. /**
  15. * 最后一个结点
  16. */
  17. private Node<E> last;
  18. /**
  19. * 表示数组大小的指标
  20. */
  21. private int current;
  22. public MyBothWayLinedList() {
  23. //初始化头结点
  24. this.head = new Node(null, null, null);
  25. this.last = null;
  26. //初始化元素个数
  27. this.current = 0;
  28. }
  29. /**
  30. * 在链表的末尾插入节点
  31. *
  32. * @param e
  33. */
  34. public void add(E e) {
  35. //如果链表为空:
  36. if (current == 0) {
  37. //创建新的结点,即,上一个节点为头节点,下一个为null
  38. Node newNode = new Node(e, head, null);
  39. //让新结点称为尾结点
  40. last = newNode;
  41. //让头结点指向尾结点
  42. head.next = last;
  43. } else {
  44. Node oldLast = last;
  45. //创建新的结点,即,上一个节点为last,下一个节点为null
  46. Node newNode = new Node(e, oldLast, null);
  47. //链接上last的节点
  48. last.next = newNode;
  49. //新节点变成为尾部节点
  50. last = newNode;
  51. }
  52. current++;
  53. }
  54. /**
  55. * 指定index位置,新增一个对象
  56. *
  57. * @param index
  58. * @param e
  59. */
  60. public void add(int index, E e) {
  61. //获取index节点
  62. Node currentNode = getNode(index);
  63. //创建新结点,并且newNode.next=indexNode.next; 即断开链表
  64. Node newNode = new Node(e, currentNode, currentNode.next);
  65. //然后插入链表节点
  66. currentNode.next = newNode;
  67. //元素的个数+1
  68. if (index == current) {
  69. last = newNode;
  70. }
  71. current++;
  72. }
  73. /**
  74. * 移除一个元素
  75. *
  76. * @param index
  77. * @return
  78. */
  79. public E remove(int index) {
  80. //找到index结点,它是待删除的
  81. Node prNode = getNode(index);
  82. Node currentNode = prNode.next;
  83. //找到index位置的下一个节点
  84. Node nextNode = currentNode.next;
  85. //删除index节点,即:前一个结点指向下一个结点
  86. currentNode.pre.next = nextNode;
  87. //元素个数-1
  88. current--;
  89. //如果删除的是末尾节点则,处理last节点指向
  90. if (index == current) {
  91. last = nextNode == null ? currentNode.pre : nextNode;
  92. }
  93. return (E) currentNode.item;
  94. }
  95. /**
  96. * 遍历当前链表,取得当前索引对应的元素
  97. *
  98. * @return
  99. */
  100. private Node<E> getNode(int index) {
  101. // 先判断索引正确性
  102. if (index > current || index < 0) {
  103. throw new RuntimeException("索引值有错:" + index + ",其范围是0-" + current);
  104. }
  105. Node<E> currentNode = head;
  106. int count = 0;
  107. while (count < index) {
  108. currentNode = currentNode.next;
  109. count++;
  110. }
  111. return currentNode;
  112. }
  113. /**
  114. * 根据索引,取得该索引下的数据
  115. *
  116. * @param index
  117. * @return
  118. */
  119. public E get(int index) {
  120. // 先判断索引正确性
  121. if (index >= current || index < 0) {
  122. throw new RuntimeException("索引值有错:" + index);
  123. }
  124. //因为头节点,不存数据,即head.next 为第一个节点
  125. Node<E> tem = head.next;
  126. int count = 0;
  127. while (count != index) {
  128. tem = tem.next;
  129. count++;
  130. }
  131. return tem.item;
  132. }
  133. public int size() {
  134. return current;
  135. }
  136. @Override
  137. public Iterator iterator() {
  138. return new MyBothWayLinedListIterator();
  139. }
  140. /**
  141. * 用来存放数据的结点型内部类
  142. */
  143. private class Node<E> {
  144. private final E item;// 结点中存放的数据
  145. private final Node<E> pre;//上一个节点
  146. private Node<E> next;// 用来指向该结点的下一个结点
  147. public Node(E item, Node pre, Node next) {
  148. this.item = item;
  149. this.pre = pre;
  150. this.next = next;
  151. }
  152. }
  153. private class MyBothWayLinedListIterator implements Iterator {
  154. private Node node;
  155. public MyBothWayLinedListIterator() {
  156. this.node = head;
  157. }
  158. @Override
  159. public boolean hasNext() {
  160. return node.next != null;
  161. }
  162. @Override
  163. public Object next() {
  164. node = node.next;
  165. return node.item;
  166. }
  167. }
  168. }

测试代码

  1. package com.ycc.data.structure.test;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.ycc.data.structure.line.MyBothWayLinedList;
  4. /**
  5. * @author liaozx
  6. * @date 2020/11/23
  7. */
  8. public class MyBothWayLinedListTest {
  9. public static void main(String[] args) {
  10. //创建顺序表对象
  11. MyBothWayLinedList<String> sl = new MyBothWayLinedList<>();
  12. //测试插入
  13. sl.add("烟雨楼0");
  14. sl.add("烟雨楼1");
  15. sl.add("烟雨楼2");
  16. sl.add(3, "烟雨楼3");
  17. sl.remove(3);
  18. System.out.println(JSONObject.toJSONString(sl));
  19. System.out.println(JSONObject.toJSONString(sl.get(0)));
  20. for (String str : sl) {
  21. System.out.println(str);
  22. }
  23. }
  24. }

3.3 时间复杂度分析

  1. get(int i):每次查询要从链表头部开始依次向后查找,数据元素越多,比较就越多,时间复杂度为 O(n)
  2. add(int i,T t):每次插入要先找到i位置的前一个元素,然后完成插入操作,数据元素越多,查找的元素越多,时间复杂度为O(n)
  3. remove(int i):每次移除要先找到i位置的前一个元素,然后完成插入操作,数据元素越多,查找的元素越多,时间复杂度为O(n)

PS:

  1. 增删改快:链表和顺序表虽然操作(插入和删除)复杂度虽然一样,但有很大优势,因为链表物理地址是不连续的,且它不需要预先指定存储空间大小(即不需要扩容+移动拷贝元素)。
  2. 查询慢:所以,查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。

    四 链表相关问题

    4.1 链表反转

    递归反转其实就是从原链表第一个存数据结点开始,依次递归调用反转每一个结点,直到把最后一个结点反转完毕,整个链表就反转完毕。

    1. //用来反转整个链表
    2. public void reverse() {
    3. //判断当前链表是否为空链表,如果是空链表,则结束运行,如果不是,则调用重载的reverse方法完成反转
    4. if (current == 0) {
    5. return;
    6. }
    7. reverse(head.next);
    8. }
    9. //反转指定的结点curr,并把反转后的结点返回
    10. public Node reverse(Node curr) {
    11. if (curr.next == null) {
    12. head.next = curr;
    13. return curr;
    14. }
    15. //递归的反转当前结点curr的下一个结点;返回值就是链表反转后,当前结点的上一个结点
    16. Node pre = reverse(curr.next);
    17. //让返回的结点的下一个结点变为当前结点curr;
    18. pre.next = curr;
    19. //把当前结点的下一个结点变为null
    20. curr.next = null;
    21. return curr;
    22. }

    4.2 快慢指针

    指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值。一般情况下,快指针的移动步长为慢指针的两倍。

    4.2.1 中间值问题

    利用快慢指针,我们把一个链表看成一个跑道,假设a的速度是b的两倍,那么当a跑完全程后,b刚好跑一半,以此来达到找到中间节点的目的。

    1. public class FastSlowTest {
    2. public static void main(String[] args) throws Exception {
    3. //创建结点
    4. Node<String> node1 = new Node<>("aa", null);
    5. Node<String> node2 = new Node<>("bb", null);
    6. Node<String> node3 = new Node<>("cc", null);
    7. Node<String> node4 = new Node<>("dd", null);
    8. Node<String> node5 = new Node<>("ee", null);
    9. Node<String> node6 = new Node<>("ff", null);
    10. Node<String> node7 = new Node<>("gg", null);
    11. //完成结点之间的指向
    12. node1.next = node2;
    13. node2.next = node3;
    14. node3.next = node4;
    15. node4.next = node5;
    16. node5.next = node6;
    17. node6.next = node7;
    18. //查找中间值
    19. String mid = getMid(node1);
    20. System.out.println("中间值为:" + mid);
    21. }
    22. /**
    23. * @param first 链表的首结点
    24. * @return 链表的中间结点的值
    25. */
    26. public static String getMid(Node<String> first) {
    27. //定义两个指针
    28. Node<String> fast = first;
    29. Node<String> slow = first;
    30. //使用两个指针遍历链表,当快指针指向的结点没有下一个结点了,就可以结束了,结束之后,慢指针指向的结点就是中间值
    31. while (fast != null && fast.next != null) {
    32. //变化fast的值和slow的值
    33. fast = fast.next.next;
    34. slow = slow.next;
    35. }
    36. return slow.item;
    37. }
    38. //结点类
    39. private static class Node<T> {
    40. //存储数据
    41. T item;
    42. //下一个结点
    43. Node next;
    44. public Node(T item, Node next) {
    45. this.item = item;
    46. this.next = next;
    47. }
    48. }
    49. }

    4.2.2 单向链表是否有环

    使用快慢指针的思想,还是把链表比作一条跑道,链表中有环,那么这条跑道就是一条圆环跑道,在一条圆环跑道中,两个人有速度差,那么迟早两个人会相遇,只要相遇那么就说明有环。 ```java package com.ycc.data.structure.test;

public class CircleListCheckTest { public static void main(String[] args) throws Exception {

  1. //创建结点
  2. Node<String> node1 = new Node<>("aa", null);
  3. Node<String> node2 = new Node<>("bb", null);
  4. Node<String> node3 = new Node<>("cc", null);
  5. Node<String> node4 = new Node<>("dd", null);
  6. Node<String> node5 = new Node<>("ee", null);
  7. Node<String> node6 = new Node<>("ff", null);
  8. Node<String> node7 = new Node<>("gg", null);
  9. //完成结点之间的指向
  10. node1.next = node2;
  11. node2.next = node3;
  12. node3.next = node4;
  13. node4.next = node5;
  14. node5.next = node6;
  15. node6.next = node7;
  16. //产生环
  17. node7.next = node3;
  18. //判断链表是否有环
  19. boolean circle = isCircle(node1);
  20. System.out.println("first链表中是否有环:" + circle);
  21. }
  22. /**
  23. * 判断链表中是否有环
  24. *
  25. * @param first 链表首结点
  26. * @return ture为有环,false为无环
  27. */
  28. public static boolean isCircle(Node<String> first) {
  29. //定义快慢指针
  30. Node<String> fast = first;
  31. Node<String> slow = first;
  32. //遍历链表,如果快慢指针指向了同一个结点,那么证明有环
  33. while (fast != null && fast.next != null) {
  34. //变换fast和slow
  35. fast = fast.next.next;
  36. slow = slow.next;
  37. if (fast.equals(slow)) {
  38. return true;
  39. }
  40. }
  41. return false;
  42. }
  43. //结点类
  44. private static class Node<T> {
  45. //存储数据
  46. T item;
  47. //下一个结点
  48. Node next;
  49. public Node(T item, Node next) {
  50. this.item = item;
  51. this.next = next;
  52. }
  53. }

}

  1. <a name="pikGj"></a>
  2. ### 4.2.3 有环链表入口问题
  3. 当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样为1,则慢指针与“新”指针相遇的地方就是环的入
  4. ```java
  5. package com.ycc.data.structure.test;
  6. /**
  7. * @author liaozx
  8. * @date 2020/11/23
  9. */
  10. public class CircleListInTest {
  11. public static void main(String[] args) throws Exception {
  12. //创建结点
  13. Node<String> node1 = new Node<>("aa", null);
  14. Node<String> node2 = new Node<>("bb", null);
  15. Node<String> node3 = new Node<>("cc", null);
  16. Node<String> node4 = new Node<>("dd", null);
  17. Node<String> node5 = new Node<>("ee", null);
  18. Node<String> node6 = new Node<>("ff", null);
  19. Node<String> node7 = new Node<>("gg", null);
  20. //完成结点之间的指向
  21. node1.next = node2;
  22. node2.next = node3;
  23. node3.next = node4;
  24. node4.next = node5;
  25. node5.next = node6;
  26. node6.next = node7;
  27. //产生环
  28. node7.next = node3;
  29. //查找环的入口结点
  30. Node<String> entrance = getEntrance(node1);
  31. System.out.println("first链表中环的入口结点元素为:" + entrance.item);
  32. }
  33. /**
  34. * 查找有环链表中环的入口结点
  35. *
  36. * @param first 链表首结点
  37. * @return 环的入口结点
  38. */
  39. public static Node getEntrance(Node<String> first) {
  40. //定义快慢指针
  41. Node<String> fast = first;
  42. Node<String> slow = first;
  43. Node<String> temp = null;
  44. //遍历链表,先找到环(快慢指针相遇),准备一个临时指针,指向链表的首结点,继续遍历,直到慢指针和临时指针相遇,那么相遇时所指向的结点就是环的入口
  45. while (fast != null && fast.next != null) {
  46. //变换快慢指针
  47. fast = fast.next.next;
  48. slow = slow.next;
  49. //判断快慢指针是否相遇
  50. if (fast.equals(slow)) {
  51. temp = first;
  52. continue;
  53. }
  54. //让临时结点变换
  55. if (temp != null) {
  56. temp = temp.next;
  57. //判断临时指针是否和慢指针相遇
  58. if (temp.equals(slow)) {
  59. break;
  60. }
  61. }
  62. }
  63. return temp;
  64. }
  65. //结点类
  66. private static class Node<T> {
  67. //存储数据
  68. T item;
  69. //下一个结点
  70. Node next;
  71. public Node(T item, Node next) {
  72. this.item = item;
  73. this.next = next;
  74. }
  75. }
  76. }

4.3 循环链表

循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个节点的指针为null,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。
image.png

4.4 约瑟夫问题

  1. public class JosephTest {
  2. public static void main(String[] args) {
  3. //解决约瑟夫问题
  4. //1.构建循环链表,包含41个结点,分别存储1~41之间的值
  5. //用来就首结点
  6. Node<Integer> first = null;
  7. //用来记录前一个结点
  8. Node<Integer> pre = null;
  9. for (int i = 1; i <= 41; i++) {
  10. //如果是第一个结点
  11. if (i == 1) {
  12. first = new Node<>(i, null);
  13. pre = first;
  14. continue;
  15. }
  16. //如果不是第一个结点
  17. Node<Integer> newNode = new Node<>(i, null);
  18. pre.next = newNode;
  19. pre = newNode;
  20. //如果是最后一个结点,那么需要让最后一个结点的下一个结点变为first,变为循环链表了
  21. if (i == 41) {
  22. pre.next = first;
  23. }
  24. }
  25. //2.需要count计数器,模拟报数
  26. int count = 0;
  27. //3.遍历循环链表
  28. //记录每次遍历拿到的结点,默认从首结点开始
  29. Node<Integer> n = first;
  30. //记录当前结点的上一个结点
  31. Node<Integer> before = null;
  32. while (n != n.next) {
  33. //模拟报数
  34. count++;
  35. //判断当前报数是不是为3
  36. if (count == 3) {
  37. //如果是3,则把当前结点删除调用,打印当前结点,重置count=0,让当前结点n后移
  38. before.next = n.next;
  39. System.out.print(n.item + ",");
  40. count = 0;
  41. n = n.next;
  42. } else {
  43. //如果不是3,让before变为当前结点,让当前结点后移;
  44. before = n;
  45. n = n.next;
  46. }
  47. }
  48. //打印最后一个元素
  49. System.out.println(n.item);
  50. }
  51. //结点类
  52. private static class Node<T> {
  53. //存储数据
  54. T item;
  55. //下一个结点
  56. Node next;
  57. public Node(T item, Node next) {
  58. this.item = item;
  59. this.next = next;
  60. }
  61. }
  62. }

五 栈

栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈。
image.png

5.1 代码实现

顺序结构的栈

  1. package com.ycc.data.structure.line;
  2. /**
  3. * @author liaozx
  4. * @description
  5. * @create 2020-11-24 10:45
  6. */
  7. public class MyArrayStack<E> {
  8. /**
  9. * 用来保存数据线性表
  10. */
  11. private final MyArrayList<E> list = new MyArrayList<E>();
  12. /**
  13. * 表示当前栈元素个数
  14. */
  15. private int size;
  16. /**
  17. * 入栈操作
  18. *
  19. * @param e
  20. */
  21. public void push(E e) {
  22. list.add(e);
  23. size++;
  24. }
  25. /**
  26. * 出栈操作
  27. *
  28. * @return
  29. */
  30. public E pop() {
  31. E e = list.get(size - 1);
  32. list.remove(size - 1);
  33. size--;
  34. return e;
  35. }
  36. public boolean isEmpty() {
  37. return size == 0;
  38. }
  39. }

链表结构的栈

  1. package com.ycc.data.structure.line;
  2. /**
  3. * @author liaozx
  4. * @description
  5. * @create 2020-11-24 10:45
  6. */
  7. public class MyLinedStack<E> {
  8. /**
  9. * 用来保存数据线性表
  10. */
  11. private final MyLinedList<E> list = new MyLinedList<E>();
  12. /**
  13. * 表示当前栈元素个数
  14. */
  15. private int size;
  16. /**
  17. * 入栈操作
  18. *
  19. * @param e
  20. */
  21. public void push(E e) {
  22. list.add(e);
  23. size++;
  24. }
  25. /**
  26. * 出栈操作
  27. *
  28. * @return
  29. */
  30. public E pop() {
  31. E e = list.get(size - 1);
  32. list.remove(size - 1);
  33. size--;
  34. return e;
  35. }
  36. public boolean isEmpty() {
  37. return size == 0;
  38. }
  39. }

5.2 测试代码

  1. package com.ycc.data.structure.test;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.ycc.data.structure.line.MyArrayStack;
  4. import com.ycc.data.structure.line.MyLinedStack;
  5. /**
  6. * @author liaozx
  7. * @date 2020/9/14
  8. */
  9. public class MyStackTest {
  10. public static void main(String[] args) {
  11. //创建栈对象
  12. MyArrayStack<String> stack = new MyArrayStack<>();
  13. //测试压栈
  14. stack.push("a");
  15. stack.push("b");
  16. stack.push("c");
  17. stack.push("d");
  18. System.out.println("---stack---" + JSONObject.toJSONString(stack));
  19. //测试弹栈
  20. String result = stack.pop();
  21. System.out.println("弹出的元素是:" + result);
  22. //创建栈对象
  23. MyLinedStack<String> myLinedStack = new MyLinedStack<>();
  24. //测试压栈
  25. myLinedStack.push("a");
  26. myLinedStack.push("b");
  27. myLinedStack.push("c");
  28. myLinedStack.push("d");
  29. System.out.println("---stack---" + JSONObject.toJSONString(myLinedStack));
  30. //测试弹栈
  31. String string = myLinedStack.pop();
  32. System.out.println("弹出的元素是:" + string);
  33. }
  34. }

5.3 栈应用-匹配括弧

  1. 创建一个栈用来存储左括号
  2. 从左往右遍历字符串,拿到每一个字符
  3. 判断该字符是不是左括号,如果是,放入栈中存储
  4. 判断该字符是不是右括号,如果不是,继续下一次循环
  5. 如果该字符是右括号,则从栈中弹出一个元素t;
  6. 判断元素t是否为null,如果不是,则证明有对应的左括号,如果不是,则证明没有对应的左括号
  7. 循环结束后,判断栈中还有没有剩余的左括号,如果有,则不匹配,如果没有,则匹配 ```java package com.ycc.data.structure.test;

import com.ycc.data.structure.line.MyLinedStack;

/**

  • @author liaozx
  • @date 2020-11-24 */ public class MyBracketsMatchTest { public static void main(String[] args) {

    1. String str = "(上海(长安)())";
    2. boolean match = isMatch(str);
    3. System.out.println(str + "中的括号是否匹配:" + match);

    }

    /**

    • 判断str中的括号是否匹配 *
    • @param str 括号组成的字符串
    • @return 如果匹配,返回true,如果不匹配,返回false */ public static boolean isMatch(String str) { //1.创建栈对象,用来存储左括号 //MyArrayStack chars = new MyArrayStack<>(); MyLinedStack chars = new MyLinedStack<>(); //2.从左往右遍历字符串 for (int i = 0; i < str.length(); i++) {

      1. String currChar = str.charAt(i) + "";
      2. //3.判断当前字符是否为左括号,如果是,则把字符放入到栈中
      3. if (currChar.equals("(")) {
      4. chars.push(currChar);
      5. } else if (currChar.equals(")")) {
      6. //4.继续判断当前字符是否是有括号,如果是,则从栈中弹出一个左括号,并判断弹出的结果是否为null,如果为null证明没有匹配的左括号,如果不为null,则证明有匹配的左括号
      7. if (chars.isEmpty()) {
      8. return false;
      9. } else {
      10. String pop = chars.pop();
      11. }
      12. }

      } //5.判断栈中还有没有剩余的左括号,如果有,则证明括号不匹配 return chars.isEmpty(); } }

  1. <a name="JKQAv"></a>
  2. ## 5.4 波兰表达式求值
  3. 1.创建一个栈对象oprands存储操作数<br />2.从左往右遍历逆波兰表达式,得到每一个字符串<br />3.判断该字符串是不是运算符,如果不是,把该该操作数压入oprands栈中<br />4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2<br />5.使用该运算符计算o1和o2,得到结果result<br />6.把该结果压入oprands栈中<br />7.遍历结束后,拿出栈中最终的结果返回
  4. ```java
  5. package com.ycc.data.structure.test;
  6. import com.ycc.data.structure.line.MyLinedStack;
  7. /**
  8. * @author liaozx
  9. * @date 2020-11-24
  10. */
  11. public class ReversePolishNotationTest {
  12. public static void main(String[] args) {
  13. //中缀表达式 3*(17-15)+18/6 的逆波兰表达式如下 6+3=9
  14. String[] expression = {"3", "17", "15", "-", "*", "18", "6", "/", "+"};
  15. int result = calculate(expression);
  16. System.out.println("逆波兰表达式的结果为:" + result);
  17. }
  18. /**
  19. * @param expression 逆波兰表达式的数组表示方式
  20. * @return 逆波兰表达式的计算结果
  21. */
  22. public static int calculate(String[] expression) {
  23. //1.定义一个栈,用来存储操作数
  24. MyLinedStack<Integer> oprands = new MyLinedStack<>();
  25. //2.从左往右遍历逆波兰表达式,得到每一个元素
  26. for (int i = 0; i < expression.length; i++) {
  27. String curr = expression[i];
  28. //3.判断当前元素是运算符还是操作数
  29. Integer o1;
  30. Integer o2;
  31. Integer result;
  32. switch (curr) {
  33. case "+":
  34. //4.运算符,从栈中弹出两个操作数,完成运算,运算完的结果再压入栈中
  35. o1 = oprands.pop();
  36. o2 = oprands.pop();
  37. result = o2 + o1;
  38. oprands.push(result);
  39. break;
  40. case "-":
  41. //4.运算符,从栈中弹出两个操作数,完成运算,运算完的结果再压入栈中
  42. o1 = oprands.pop();
  43. o2 = oprands.pop();
  44. result = o2 - o1;
  45. oprands.push(result);
  46. break;
  47. case "*":
  48. //4.运算符,从栈中弹出两个操作数,完成运算,运算完的结果再压入栈中
  49. o1 = oprands.pop();
  50. o2 = oprands.pop();
  51. result = o2 * o1;
  52. oprands.push(result);
  53. break;
  54. case "/":
  55. //4.运算符,从栈中弹出两个操作数,完成运算,运算完的结果再压入栈中
  56. o1 = oprands.pop();
  57. o2 = oprands.pop();
  58. result = o2 / o1;
  59. oprands.push(result);
  60. break;
  61. default:
  62. //5.操作数,把该操作数放入到栈中;
  63. oprands.push(Integer.parseInt(curr));
  64. break;
  65. }
  66. }
  67. //6.得到栈中最后一个元素,就是逆波兰表达式的结果
  68. int result = oprands.pop();
  69. return result;
  70. }
  71. }

5.5 波兰表达式(补充)

前缀表达式(即波兰表达式)

概念

前缀表达式是一种没有括号的算术表达式,与中缀表达式不同的是,其将运算符写在前面,操作数写在后面。

举例说明

  • (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6

前缀表达式的计算机求值

  • 从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果

前缀表达式求值步骤示例

(3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6

1)、步骤图解如下

数据结构-通识篇之线性表 - 图6
2)、步骤描述如下

  • 从右至左扫描,将6、5、4、3压入堆栈
  • 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
  • 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
  • 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式

概念

  • 中缀表达式是一个通用的算术或逻辑公式表示方法。

举例说明

  • 中缀表达式就是常见的运算表达式,如(3+4)×5-6

中缀表达式的计算机求值

  • 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作,因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)

后缀表达式(即逆波兰表达式)

概念

  • 后缀表达式一般指逆波兰式 ,逆波兰式(Reverse Polish notation,RPN,或逆波兰记法),也叫后缀表达式(将运算符写在操作数之后)

举例说明

  • (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –

数据结构-通识篇之线性表 - 图7

后缀表达式的计算机求值

  • 从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果

后缀表达式求值步骤示例

(3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 -

1)、步骤图解如下
数据结构-通识篇之线性表 - 图8数据结构-通识篇之线性表 - 图9
2)、步骤描述如下

  • 从左至右扫描,将3和4压入堆栈;
  • 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
  • 将5入栈;
  • 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
  • 将6入栈;
  • 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

六 队列

队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先读被读出来。
image.png

6.1 代码实现

顺序结构的队列

  1. package com.ycc.data.structure.line;
  2. /**
  3. * @author liaozx
  4. * @description
  5. * @create 2020-11-24
  6. */
  7. public class MyArrayQueue<E> {
  8. /**
  9. * 用来保存数据的队列
  10. */
  11. private final MyArrayList<E> list = new MyArrayList<E>();
  12. /**
  13. * 表示当前栈元素个数
  14. */
  15. private int size;
  16. /**
  17. * 入队
  18. *
  19. * @param e
  20. */
  21. public void enQueue(E e) {
  22. list.add(e);
  23. size++;
  24. }
  25. /**
  26. * 出队
  27. *
  28. * @return
  29. */
  30. public E deQueue() {
  31. if (size > 0) {
  32. E e = list.get(0);
  33. list.remove(0);
  34. return e;
  35. } else {
  36. throw new RuntimeException("已经到达队列顶部");
  37. }
  38. }
  39. }

链表结构的队列

  1. package com.ycc.data.structure.line;
  2. /**
  3. * @author liaozx
  4. * @description
  5. * @create 2020-11-24 10:57
  6. */
  7. public class MyLinedQueue<E> {
  8. private final MyLinedList<E> list = new MyLinedList<E>();// 用来保存数据的队列
  9. private int size;// 表示当前栈元素个数
  10. /**
  11. * 入队
  12. *
  13. * @param e
  14. */
  15. public void enQueue(E e) {
  16. list.add(e);
  17. size++;
  18. }
  19. /**
  20. * 出队
  21. *
  22. * @return
  23. */
  24. public E deQueue() {
  25. if (size > 0) {
  26. E e = list.get(0);
  27. list.remove(0);
  28. return e;
  29. } else {
  30. throw new RuntimeException("已经到达队列顶部");
  31. }
  32. }
  33. }

6.2 测试代码

  1. package com.ycc.data.structure.test;
  2. import com.ycc.data.structure.line.MyArrayQueue;
  3. import com.ycc.data.structure.line.MyLinedQueue;
  4. /**
  5. * @author liaozx
  6. * @date 2020/11/24
  7. */
  8. public class MyQueueTest {
  9. public static void main(String[] args) {
  10. //创建队列对象
  11. MyArrayQueue<String> myArrayQueue = new MyArrayQueue<>();
  12. //测试队列的enqueue方法
  13. myArrayQueue.enQueue("a");
  14. myArrayQueue.enQueue("b");
  15. myArrayQueue.enQueue("c");
  16. myArrayQueue.enQueue("d");
  17. System.out.println("-------------------------------");
  18. //测试队列的dequeue方法
  19. String result = myArrayQueue.deQueue();
  20. System.out.println("出队列的元素是:" + result);
  21. //创建队列对象
  22. MyLinedQueue<String> myLinedQueue = new MyLinedQueue<>();
  23. //测试队列的enqueue方法
  24. myLinedQueue.enQueue("a");
  25. myLinedQueue.enQueue("b");
  26. myLinedQueue.enQueue("c");
  27. myLinedQueue.enQueue("d");
  28. System.out.println("-------------------------------");
  29. //测试队列的dequeue方法
  30. String string = myLinedQueue.deQueue();
  31. System.out.println("出队列的元素是:" + string);
  32. }
  33. }

七 符号表

符号表最主要的目的就是将一个键和一个值联系起来,符号表能够将存储的数据元素是一个键和一个值共同组成的键值对数据,我们可以根据键来查找对应的值。
image.png

7.1 无序符号表

7.1.1 代码实现

  1. package com.ycc.data.structure.line;
  2. /**
  3. * 链表结构的符号表
  4. * 无序
  5. *
  6. * @author liaozx
  7. * @date 2020/11/24
  8. */
  9. public class MySymbolTable<Key, Value> {
  10. //记录首结点
  11. private Node head;
  12. //记录符号表中元素的个数
  13. private int current;
  14. private class Node {
  15. //键
  16. public Key key;
  17. //值
  18. public Value value;
  19. //下一个结点
  20. public Node next;
  21. public Node(Key key, Value value, Node next) {
  22. this.key = key;
  23. this.value = value;
  24. this.next = next;
  25. }
  26. }
  27. public MySymbolTable() {
  28. this.head = new Node(null, null, null);
  29. this.current = 0;
  30. }
  31. //获取符号表中键值对的个数
  32. public int size() {
  33. return current;
  34. }
  35. //往符号表中插入键值对
  36. public void put(Key key, Value value) {
  37. //符号表中已经存在了键为key的键值对,那么只需要找到该结点,替换值为value即可
  38. Node node = head;
  39. while (node.next != null) {
  40. //变换node
  41. node = node.next;
  42. //判断n结点存储的键是否为key,如果是,则替换变换node结点的值
  43. if (node.key.equals(key)) {
  44. node.value = value;
  45. return;
  46. }
  47. }
  48. //如果符号表中不存在键为key的键值对,只需要创建新的结点,保存要插入的键值对,把新结点插入到链表的头部 head.next=新结点即可
  49. Node newNode = new Node(key, value, null);
  50. //头部第一个有效节点
  51. Node oldFirst = head.next;
  52. newNode.next = oldFirst;
  53. head.next = newNode;
  54. //元素个数+1;
  55. current++;
  56. }
  57. //删除符号表中键为key的键值对
  58. public void delete(Key key) {
  59. //找到键为key的结点,把该结点从链表中删除
  60. Node node = head;
  61. while (node.next != null) {
  62. //判断node结点的下一个结点的键是否为key,如果是,就删除该结点
  63. if (node.next.key.equals(key)) {
  64. node.next = node.next.next;
  65. current--;
  66. return;
  67. }
  68. //变换node
  69. node = node.next;
  70. }
  71. }
  72. //从符号表中获取key对应的值
  73. public Value get(Key key) {
  74. //找到键为key的结点
  75. Node node = head;
  76. while (node.next != null) {
  77. //变换n
  78. node = node.next;
  79. if (node.key.equals(key)) {
  80. return node.value;
  81. }
  82. }
  83. return null;
  84. }
  85. }

7.1.2 测试代码

  1. package com.ycc.data.structure.test;
  2. import com.ycc.data.structure.line.MySymbolTable;
  3. /**
  4. * @author liaozx
  5. * @date 2020/11/24
  6. */
  7. public class MySymbolTableTest {
  8. public static void main(String[] args) {
  9. //创建符号表对象
  10. MySymbolTable<Integer, String> symbolTable = new MySymbolTable<>();
  11. //测试put方法(插入,替换)
  12. symbolTable.put(1, "yanyulou1");
  13. symbolTable.put(2, "yanyulou2");
  14. symbolTable.put(3, "yanyulou3");
  15. symbolTable.put(2, "yanyulou4");
  16. System.out.println("替换完毕后的元素的个数为:" + symbolTable.size());
  17. //测试get方法
  18. System.out.println("替换完毕后,键2对应的值为:" + symbolTable.get(2));
  19. //测试删除方法
  20. symbolTable.delete(2);
  21. System.out.println("删除完毕后,元素的个数:" + symbolTable.size());
  22. }
  23. }

7.2 有序符号表

我们需要根据键的大小进行排序,插入数据时要考虑顺序

7.2.1 代码实现

  1. package com.ycc.data.structure.line;
  2. /**
  3. * @author liaozx
  4. * @date 2020/11/24
  5. */
  6. public class MyOrderSymbolTable<Key extends Comparable<Key>, Value> {
  7. //记录首结点
  8. private Node head;
  9. //记录符号表中元素的个数
  10. private int current;
  11. private class Node {
  12. //键
  13. public Key key;
  14. //值
  15. public Value value;
  16. //下一个结点
  17. public Node next;
  18. public Node(Key key, Value value, Node next) {
  19. this.key = key;
  20. this.value = value;
  21. this.next = next;
  22. }
  23. }
  24. public MyOrderSymbolTable() {
  25. this.head = new Node(null, null, null);
  26. this.current = 0;
  27. }
  28. //获取符号表中键值对的个数
  29. public int size() {
  30. return current;
  31. }
  32. //往符号表中插入键值对
  33. public void put(Key key, Value value) {
  34. //定义两个Node变量,分别记录当前结点和当前结点的上一个结点
  35. Node curr = head.next;
  36. Node pre = head;
  37. //1.如果key大于当前结点的key,则一直寻找下一个结点
  38. while (curr != null && key.compareTo(curr.key) > 0) {
  39. //变换当前结点和前一个结点即可
  40. pre = curr;
  41. curr = curr.next;
  42. }
  43. //如果当前结点curr的键和要插入的key一样,则替换
  44. if (curr != null && key.compareTo(curr.key) == 0) {
  45. curr.value = value;
  46. return;
  47. }
  48. //如果当前结点curr的键和要插入的key不一样,把新的结点插入到curr之前
  49. Node newNode = new Node(key, value, curr);
  50. pre.next = newNode;
  51. //元素的个数+1;
  52. current++;
  53. }
  54. //删除符号表中键为key的键值对
  55. public void delete(Key key) {
  56. //找到键为key的结点,把该结点从链表中删除
  57. Node node = head;
  58. while (node.next != null) {
  59. //判断n结点的下一个结点的键是否为key,如果是,就删除该结点
  60. if (node.next.key.equals(key)) {
  61. node.next = node.next.next;
  62. current--;
  63. return;
  64. }
  65. //变换n
  66. node = node.next;
  67. }
  68. }
  69. //从符号表中获取key对应的值
  70. public Value get(Key key) {
  71. //找到键为key的结点
  72. Node node = head;
  73. while (node.next != null) {
  74. //变换n
  75. node = node.next;
  76. if (node.key.equals(key)) {
  77. return node.value;
  78. }
  79. }
  80. return null;
  81. }
  82. }

7.2.2 测试代码

  1. package com.ycc.data.structure.test;
  2. import com.ycc.data.structure.line.MyOrderSymbolTable;
  3. /**
  4. * @author liaozx
  5. * @date 2020/11/24
  6. */
  7. public class MyOrderSymbolTableTest {
  8. public static void main(String[] args) {
  9. //创建有序符号表对象
  10. MyOrderSymbolTable<Integer, String> table = new MyOrderSymbolTable<>();
  11. table.put(1, "张1");
  12. table.put(2, "张2");
  13. table.put(4, "张4");
  14. table.put(7, "张7");
  15. table.put(3, "张3");
  16. System.out.println("table" + table);
  17. }
  18. }

7.3 跳跃表

跳跃表(SkipList)是一种可以替代平衡树的数据结构。跳跃表让已排序的数据分布在多层次的链表结构中,默认是将 Key值升序排列的,以0-1 的随机值决定一个数据是否能够攀升到高层次的链表中。它通过容许一定的数据冗余,达到 “以空间换时间” 的目的。跳跃表的效率和AVL相媲美,查找、添加、插入、删除操作都能够在 O(LogN) 的复杂度内完成。

image.png

  • 一个跳跃表应该有若干个层(Level)链表组成;
  • 跳跃表中最底层的链表包含所有数据, 每一层链表中的数据都是有序的;
  • 如果一个元素X出现在第i层,那么编号比 i 小的层都包含元素 X;
  • 第 i 层的元素通过一个指针指向下一层拥有相同值的元素;
  • 在每一层中,-∞ 和 +∞ 两个元素都出现(分别表示 INT_MIN 和 INT_MAX);
  • 头指针(head)指向最高一层的第一个元素;

节点模型
image.png

7.3.1 代码实现

主要实现功能:

  • get(Integer key) : 根据 key 值查找某个元素
  • put(Integer key, Object value) :插入一个新的元素,元素已存在时为修改操作
  • remove(Integer key): 根据 key 值删除某个元素 ```java package com.ycc.data.structure.line;

import java.util.Random;

/**

  • 跳跃表 *
  • @author liaozx
  • @date 2020/10/24 */ public class MySkipList {

    // 节点数量 private int number; // 节点最大层数 private int height;

    // 第一个节点 private SkipListEntry head; // 最后一个节点 private SkipListEntry tail;

    private Random random;

    private class SkipListEntry {

    1. // data
    2. public Integer key;
    3. public E value;
    4. // links
    5. public SkipListEntry up;
    6. public SkipListEntry down;
    7. public SkipListEntry left;
    8. public SkipListEntry right;
    9. // constructor
    10. public SkipListEntry(Integer key, E value) {
    11. this.key = key;
    12. this.value = value;
    13. }

    }

    public MySkipList() {

    1. // 创建 head 节点
    2. this.head = new SkipListEntry(Integer.MIN_VALUE, null);
    3. // 创建 tail 节点
    4. this.tail = new SkipListEntry(Integer.MAX_VALUE, null);
    5. // 将 head 节点的右指针指向 tail 节点
    6. this.head.right = tail;
    7. // 将 tail 节点的左指针指向 head 节点
    8. this.tail.left = head;
    9. this.height = 0;
    10. this.number = 0;
    11. this.random = new Random();

    }

    public Object get(Integer key) {

    1. SkipListEntry p = findEntry(key);
    2. if (p.key.equals(key)) {
    3. return p.value;
    4. } else {
    5. return null;
    6. }

    }

    public Object put(Integer key, Object value) {

    1. SkipListEntry p, q;
    2. int i = 0;
    3. // 查找适合插入的位子
    4. p = findEntry(key);
    5. // 如果跳跃表中存在含有key值的节点,则进行value的修改操作即可完成
    6. if (p.key.equals(key)) {
    7. Object oldValue = p.value;
    8. p.value = value;
    9. return oldValue;
    10. }
    11. // 如果跳跃表中不存在含有key值的节点,则进行新增操作
    12. q = new SkipListEntry(key, value);
    13. q.left = p;
    14. q.right = p.right;
    15. p.right.left = q;
    16. p.right = q;
    17. // 再使用随机数决定是否要向更高level攀升 , 抛硬币, 50% 概率
    18. while (random.nextDouble() < 0.5) {
    19. // 如果新元素的级别已经达到跳跃表的最大高度,则新建空白层
    20. if (i >= height) {
    21. addEmptyLevel();
    22. }
    23. //从p向左扫描含有高层节点的节点
    24. while (p.up == null) {
    25. p = p.left;
    26. }
    27. p = p.up;
    28. // 新增和q指针指向的节点含有相同key值的节点对象
    29. // 这里需要注意的是除底层节点之外的节点对象是不需要value值的
    30. SkipListEntry z = new SkipListEntry(key, null);
    31. z.left = p;
    32. z.right = p.right;
    33. p.right.left = z;
    34. p.right = z;
    35. z.down = q;
    36. q.up = z;
    37. q = z;
    38. i = i + 1;
    39. }
    40. number = number + 1;
    41. // 返回null,没有旧节点的value值
    42. return null;

    }

    public Object remove(Integer key) {

    1. SkipListEntry p, q;
    2. p = findEntry(key);
    3. if (!p.key.equals(key)) {
    4. return null;
    5. }
    6. Object oldValue = p.value;
    7. while (p != null) {
    8. q = p.up;
    9. p.left.right = p.right;
    10. p.right.left = p.left;
    11. p = q;
    12. }
    13. return oldValue;

    }

  1. /**
  2. * 创建新的空索引层
  3. */
  4. private void addEmptyLevel() {
  5. SkipListEntry p1, p2;
  6. p1 = new SkipListEntry(Integer.MIN_VALUE, null);
  7. p2 = new SkipListEntry(Integer.MAX_VALUE, null);
  8. p1.right = p2;
  9. p1.down = head;
  10. p2.left = p1;
  11. p2.down = tail;
  12. head.up = p1;
  13. tail.up = p2;
  14. head = p1;
  15. tail = p2;
  16. height = height + 1;
  17. }
  18. private SkipListEntry findEntry(Integer key) {
  19. // 从head头节点开始查找
  20. SkipListEntry p = head;
  21. while (true) {
  22. // 从左向右查找,直到右节点的key值大于要查找的key值
  23. while (p.right.key <= key) {
  24. p = p.right;
  25. }
  26. // 如果有更低层的节点,则向低层移动
  27. if (p.down != null) {
  28. p = p.down;
  29. } else {
  30. break;
  31. }
  32. }
  33. // 返回p,!注意这里p的key值是小于等于传入key的值的(p.key <= key)
  34. return p;
  35. }

}

  1. <a name="M6f9V"></a>
  2. ### 7.3.2 测试代码
  3. ```java
  4. package com.ycc.data.structure.test;
  5. import com.ycc.data.structure.line.MySkipList;
  6. /**
  7. * @author liaozx
  8. * @date 2020/11/24
  9. */
  10. public class MySkipListTest {
  11. public static void main(String[] args) {
  12. MySkipList mySkipList = new MySkipList();
  13. mySkipList.put(1, "yanyu0");
  14. mySkipList.put(2, "yanyu1");
  15. mySkipList.put(3, "yanyu2");
  16. mySkipList.put(4, "yanyu3");
  17. mySkipList.put(5, "yanyu4");
  18. mySkipList.put(6, "yanyu5");
  19. System.out.println("mySkipList" + mySkipList);
  20. }
  21. }