前言

链表是一种 递归 的数据结构,或者为空 null,或者指向一个结点(node)的引用,一个结点含有 一个泛型元素和一个指向另一条链表的引用

通常分为如下三种类型:

  • 单向链表:结点被分成两个部分。第一个部分保存或者显示关于结点的信息,第二个部分存储下一个结点的地址,只能向一个方向遍历。
  • 双向链表:每个数据结点中都有两个指针,分别指向直接后继和直接前驱。
  • 循环链表:一种 链式存储结构,它的最后一个结点指向头结点,形成一个环。

单向链表

单向链表包括一个值和一个指向下一结点的指针,其典型结构定义如下:

链表解惑 - 图1

  1. public class Node{
  2. // 数据对象
  3. private Object val;
  4. // 指向后继结点
  5. private Node next;
  6. // 无参构造函数
  7. public Node(){
  8. // 指向数据对象和后继结点的引用都置空
  9. this.val = null;
  10. this.next = null;
  11. }
  12. // 有参构造函数
  13. public Node(Object val, Node node){
  14. this.val = val;
  15. this.next = node;
  16. }
  17. // 获取当前存放位置的数据对象
  18. public Object getVal(){
  19. return this.val;
  20. }
  21. // 将给定元素存放至当前位置,同时返回此前存放的数据对象
  22. public Object setVal(Object value){
  23. Object oldVal = this.val;
  24. this.val = value;
  25. return oldVal;
  26. }
  27. // 取当前结点后继结点
  28. public Node getNext(){
  29. return this.next;
  30. }
  31. // 修改当前结点后继结点
  32. public void setNext(Node newNext){
  33. this.next = newNext;
  34. }
  35. }

双向链表

每个数据结点中都有两个指针,分别指向直接后继和直接前驱,其典型结构定义如下:

链表解惑 - 图2

  1. public class DoubleNode{
  2. // 数据对象
  3. private Object val;
  4. // 指向前驱结点
  5. private DoubleNode prev;
  6. // 指向后继结点
  7. private DoubleNode next;
  8. // 无参构造函数
  9. public DoubleNode(){
  10. this.val = null;
  11. this.prev = null;
  12. this.next = null;
  13. }
  14. // 有参构造函数
  15. public DoubleNode(Object value, DoubleNode previous, DoubleNode next){
  16. this.val = value;
  17. this.prev = previous;
  18. this.next = next;
  19. }
  20. // 获取当前位置数据对象
  21. public Object getVal(){
  22. return val;
  23. }
  24. // 将给定元素放在当前位置,返回此前存放的元素
  25. public Object setVal(Object value){
  26. Object oldVal = val;
  27. val = value;
  28. return oldVal;
  29. }
  30. // 获取当前结点后继结点
  31. public DoubleNode getNext(){
  32. return next;
  33. }
  34. // 修改后继结点
  35. public void setNext(DoubleNode newNext){
  36. next = newNext;
  37. }
  38. // 获取当前位置前驱结点
  39. public DoubleNode getPrev(){
  40. return prev;
  41. }
  42. // 修改前驱结点
  43. public void setPrev(DoubleNode newPrev){
  44. prev = newPrev;
  45. }
  46. }

单向链表的增删改查

基于链表实现栈

  1. public class MyStack{
  2. // 指向栈顶元素(头结点)
  3. private Node head;
  4. // 栈中元素数目
  5. private int size;
  6. // 构造方法(构造一个空栈)
  7. public MyStack(){
  8. this.head = null;
  9. this.size = 0;
  10. }
  11. // 查询栈中元素个数
  12. public int getSize(){
  13. return size;
  14. }
  15. // 判断栈是否为空
  16. public boolean isEmpty(){
  17. return head == null;
  18. // 或者 size == 0;
  19. }
  20. // 读取栈顶(首结点信息)
  21. public Object getTop(){
  22. if(isEmpty()){
  23. System.out.println("栈空");
  24. }
  25. return head.getVal();
  26. }
  27. // 压栈(即插入首结点)
  28. public void insertAtHead(Object val){
  29. // 创建一个新结点,将其作为首结点插入
  30. Node node = new Node(val, head);
  31. // 更新首结点引用
  32. head = node;
  33. // 栈中元素数目增加
  34. size++;
  35. }
  36. // 出栈(删除首结点)
  37. public Object removeAtHead(){
  38. if(isEmpty){
  39. System.out.println("栈空");
  40. }
  41. // 当前结点数据对象
  42. Object tmp = head.getVal();
  43. // 更新首结点引用
  44. head = head.getNext();
  45. size--;
  46. return tmp;
  47. }
  48. }

基于链表实现队列

  1. public class MyQueue{
  2. // 队列首元素(首结点)
  3. private Node head;
  4. // 队列尾元素(尾结点)
  5. private Node tail;
  6. // 队列规模
  7. private int size;
  8. // 构造空队列
  9. public MyQueue(){
  10. head = null;
  11. tail = null;
  12. size = 0;
  13. }
  14. // 队列规模
  15. public int getSize(){
  16. return size;
  17. }
  18. // 判断当前队列是否为空
  19. public boolean isEmpty(){
  20. return size == 0;
  21. }
  22. // 查看队首元素
  23. public Object getFront(){
  24. if(isEmpty()){
  25. System.out.println("队列为空");
  26. }
  27. return head.getElem();
  28. }
  29. // 入队
  30. public void enQueue(Object val){
  31. Node node = new Node();
  32. // 新结点作为末结点插入
  33. node.setVal(val);
  34. node.setNext(null);
  35. // 若当前队列为空,则直接插入
  36. if(size == 0){
  37. head = node;
  38. }else {
  39. // 否则将新结点接至队列末端
  40. tail.setNext(node);
  41. }
  42. // 更新指向末结点的引用
  43. tail = node;
  44. // 更新队列规模
  45. size++;
  46. }
  47. // 出队
  48. public Object deQueue(){
  49. if(size == 0){
  50. System.out.println("队列为空");
  51. }
  52. Object tmp = head.getVal();
  53. head = head.getNext();
  54. // 更新队列规模
  55. size--;
  56. // 队列为空时,将末结点引用置空
  57. if(size == 0){
  58. tail = null;
  59. }
  60. return tmp;
  61. }
  62. // 遍历队列
  63. public void traversal(){
  64. Node node = head;
  65. while(node != null){
  66. System.out.println(node.getVal() + "\t");
  67. node = node.getNext();
  68. }
  69. }
  70. }

双向链表的增删改查

实现双向链表时,通常在最前端和最后端各设置一个 哑元结点,分别称为 头结点尾结点,起着 哨兵 的作用。但实际上两者并不存储任何实质的数据对象,头(尾)结点的 nextprev)引用指向首(末)结点,而 prevnext)引用为空。

首尾结点的插入

假设要进行首结点的插入,则通常需要如下步骤,而末结点的插入则是和首结点的插入过程对称。

  1. 首先生成一个新结点;
  2. 然后将其接入队列的前端;
  3. 接着将头结点的 next 的引用指向新插入的结点,同时将首结点的 prev 的引用指向新插入的结点。

首尾结点的删除

假设要进行末结点的删除,通常需要如下步骤,而首结点的删除过程适合尾结点的删除过程对称。

  1. 将新的末结点的 next 引用指向尾结点;
  2. 同时将尾结点的 prev 引用指向新的末结点;
  3. 最后原先的末结点将会被系统回收。

一般结点的插入与删除

要实现在一般结点之间插入新结点,通常需要进行如下步骤:

  1. 创建一个新的结点,然后将其 prev 引用指向前一个结点,同时将其 next 引用指向后一个结点;
  2. 然后将前一个结点的 next 引用指向新结点,同时将后一个结点的 prev 引用指向新结点。

而要实现在一般结点之间删除结点,通常需要进行如下步骤:

  1. 首先找到要删除的结点的前驱和后继结点;
  2. 然后将其前驱结点的 next 引用指向后驱结点,同时将后驱结点的 prev 引用指向前驱结点。
  1. public class MyDoubleQueue{
  2. // 指向头结点(哨兵)
  3. private DoubleNode header;
  4. // 指向尾结点(哨兵)
  5. private DoubleNode trailer;
  6. // 队列规模
  7. private int size;
  8. // 无参构造函数
  9. public MyDoubleQueue(){
  10. header = new DoublNode();
  11. trailer = new DoubleNode();
  12. header.setNext(trailer);
  13. trailer.setPrev(trailer);
  14. size = 0;
  15. }
  16. // 获取当前队列规模
  17. public int getSize(){
  18. return size;
  19. }
  20. // 判断队列是否为空
  21. public boolean isEmpty(){
  22. return size == 0;
  23. }
  24. // 取首元素
  25. public Object getHead(){
  26. if(isEmpty()){
  27. System.out.println("队列为空");
  28. }
  29. return header.getNext().getVal();
  30. }
  31. // 取末元素
  32. public Objecct getTrail(){
  33. if(isEmpty()){
  34. System.out.println("队列为空");
  35. }
  36. return trailer.getPrev().getVal();
  37. }
  38. // 遍历
  39. public void traversal(){
  40. DoubleNode node = header.getNext();
  41. while(node != trailer){
  42. System.out.print(node.getVal() + "\t");
  43. node = node.getNext();
  44. }
  45. System.out.println();
  46. }
  47. // 前端插入新结点
  48. public void insertFirst(Object val){
  49. DoubleNode second = header.getNext();
  50. DoubleNode first = new DoubleNode(val, header, second);
  51. second.setPrev(first);
  52. header.setNext(first)
  53. size++;
  54. }
  55. // 前端删除结点
  56. public Object removeFirst(){
  57. if(isEmpty()){
  58. System.out.println("队列为空");
  59. }
  60. // first 是要删除的结点,找到要删除结点的后继结点 second
  61. DoubleNode first = header.getNext();
  62. DoubleNode second = first.getNext();
  63. // 要删除结点的值
  64. Object value = first.getVal();
  65. // 将头结点的 next 指向要删除的后继结点 second,同时将 second 的前驱结点指向头结点
  66. header.setNext(second);
  67. second.setPrev(header);
  68. // 更新规模
  69. size--;
  70. return value;
  71. }
  72. // 后端插入新结点
  73. public void insertLast(Object val){
  74. DoubleNode second = trailer.getPrev();
  75. DoubleNode first = new DoubleNode(val, second, trailer);
  76. second.setNext(first);
  77. trailer.setPrev(first);
  78. size++;
  79. }
  80. // 后端删除结点
  81. public Object removeLast(){
  82. if(isEmpty()){
  83. System.out.println("队列为空");
  84. }
  85. DoubleNode first = trailer.getPrev();
  86. DoubleNode second = first.getPrev();
  87. // 要删除结点的值
  88. Object value = first.getVal();
  89. // 将尾结点的 prev 指向要删除的前驱结点 second,同时将 second 的后继结点指向尾结点
  90. trailer.setPrev(second);
  91. second.setNext(trailer);
  92. // 更新规模
  93. size--;
  94. return value;
  95. }
  96. }

总结

本文从单向链表和双向链表的结构定义出发,然后又分别介绍了如何基于单向链表实现堆和栈,最后则是对双向链表的增删改查进行了总结。对于文中有疏漏的地方,欢迎评论留言。如果你觉得文章对你有所帮助,那就点个赞再走吧!