链表

链表是一种非常重要的线性数据结构,我们在实现栈和队列时使用的是动态数组实现的,这个动态数组是针对用户而言是动态的,实际上底层是静态的,是通过resize()操作去解决容量问题的。而链表则是一种真正的动态数据结构,它是这么一种数据结构,我们把数据存储在一个节点(Node)中,一个节点一般包含两部分的内容,一个是存储的数据,一个是它要指向的下一个节点

  1. class Node {
  2. private E e;
  3. private Node next;
  4. }

一个节点指向一个节点,所以最后看起来就像是一个链,我们把这种数据结构称为链表
链表 - 图1
最后一个节点的下一个节点为NULL,表示后面没有节点了。它是一个真正的动态的数据结构,不需要处理容量的问题。但是它也有缺点,它没有数组那样快的查询能力,它要查询某个节点的数据,只能通过头结点一直寻找下来(后面我们将看到),所以它的查询速度比数组慢。

链表实现

现在我们将实现这么一个结构,首先设计好节点类

  1. public class LinkedList<E> {
  2. //我们将Node设置为LinkedList的私有内部类
  3. private class Node<E> {
  4. public E e;
  5. public Node next;
  6. public Node(E e, Node next) {
  7. this.e = e;
  8. this.next = next;
  9. }
  10. public Node(E e) {
  11. this(e, null);
  12. }
  13. public Node() {
  14. this(null, null);
  15. }
  16. @Override
  17. public String toString() {
  18. return e.toString();
  19. }
  20. }
  21. }

我们要想向链表中添加(或其他操作)元素,不可避免的要遍历链表(因为链表不能通过索引访问,只能通过前面的节点找到后面的节点),而要遍历链表,我们就要将链表的头存储起来,这样才能遍历链表,我们将链表的头称为head
链表 - 图2
同时我们使用变量size来记录链表中元素的个数

  1. public class LinkedList<E> {
  2. //为了节省篇幅,Node类不再展示,下同
  3. //头结点
  4. private Node head;
  5. //链表中元素的个数
  6. private int size;
  7. public LinkedList() {
  8. head = null;
  9. size = 0;
  10. }
  11. }

现在我们实现两个简单的方法getSize()和isEmpty()

  1. public int getSize() {
  2. return size;
  3. }
  4. public boolean isEmpty() {
  5. return size == 0;
  6. }

添加元素

向链表头添加元素

链表 - 图3
首先将要插入的新节点指向head,然后将head设置为新节点,实现如下

  1. public void addFirst(E e) {
  2. //体会一下这条语句的意思
  3. head = new Node(e,head);
  4. size++;
  5. }

在链表的中间添加一个元素

链表 - 图4
比如现在往节点1后面插入一个元素,首先将新节点指向节点2,然后节点1指向新节点,实现如下

  1. public void add(int index, E e) {
  2. if (index < 0 || index > size) {
  3. throw new IllegalArgumentException("参数错误");
  4. }
  5. //如果是头结点需要单独处理
  6. if (index == 0) {
  7. addFirst(e);
  8. }
  9. //prev代表要插入位置的前一个节点
  10. Node prev = head;
  11. for (int i = 0; i < index - 1; i++) {
  12. prev = prev.next;
  13. }
  14. prev.next = new Node(e, prev.next);
  15. size++;
  16. }

向链表的尾部添加一个元素

直接复用上面的代码

  1. public void addLast(E e) {
  2. add(size, e);
  3. }

虚拟头结点

我们在向链表中添加元素时,因为head前面没有节点,所以我们在添加元素时会对head进行单独的处理,为了不使head具有特殊性,我们在链表的最头部添加一个虚拟头结点,里面不存储元素,它的存在是为了使得操作链表方便
链表 - 图5
现在我们修改上面的head为dummyHead

  1. public class LinkedList<E> {
  2. //虚拟结点
  3. private Node dummyHead;
  4. private int size;
  5. public LinkedList() {
  6. //这里修改了
  7. dummyHead = new Node(null, null);
  8. size = 0;
  9. }
  10. public int getSize() {
  11. return size;
  12. }
  13. public boolean isEmpty() {
  14. return size == 0;
  15. }
  16. //直接调用add方法
  17. public void addFirst(E e) {
  18. add(0,e);
  19. }
  20. public void add(int index, E e) {
  21. if (index < 0 || index > size) {
  22. throw new IllegalArgumentException("参数错误");
  23. }
  24. //不需要对head进行单独的处理了
  25. //index - 1修改为了index
  26. Node prev = dummyHead;
  27. for (int i = 0; i < index; i++) {
  28. prev = prev.next;
  29. }
  30. prev.next = new Node(e, prev.next);
  31. size++;
  32. }
  33. public void addLast(E e) {
  34. add(size, e);
  35. }
  36. }

获得某个索引的值

实现的思路同add很像,不过这里我们找的不是前一个节点,而是当前的节点

  1. public E get(int index) {
  2. if (index < 0 || index >= size) {
  3. throw new IllegalArgumentException("参数错误");
  4. }
  5. //cur代表当前节点
  6. Node cur = dummyHead.next;
  7. for (int i = 0; i < index; i++) {
  8. cur = cur.next;
  9. }
  10. return (E) cur.e;
  11. }

基于这个方法,我们可以很快的实现getFirst()和getLast()

  1. public E getFirst() {
  2. return get(0);
  3. }
  4. public E getLast() {
  5. return get(size - 1);
  6. }

更新某个索引的值

实现的思路完全是同get()方法,直接上代码

  1. public void set(int index, E e) {
  2. if (index < 0 || index >= size) {
  3. throw new IllegalArgumentException("参数错误");
  4. }
  5. Node cur = dummyHead.next;
  6. for (int i = 0; i < index; i++) {
  7. cur = cur.next;
  8. }
  9. cur.e = e;
  10. }

查找链表是否存在元素e

  1. public boolean contains(E e) {
  2. //从当前节点开始,一直遍历到最后一个节点
  3. for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
  4. if (cur.e.equals(e)) {
  5. return true;
  6. }
  7. }
  8. return false;
  9. }

删除链表中的元素

链表 - 图6
上图已详细说明了操作的步骤,这里直接贴上代码实现

  1. public E remove(int index) {
  2. if (index < 0 || index >= size) {
  3. throw new IllegalArgumentException("参数错误");
  4. }
  5. //获得要删除节点的前一个节点
  6. Node prev = dummyHead;
  7. for (int i = 0; i < index; i++) {
  8. prev = prev.next;
  9. }
  10. //图示的操作
  11. Node delNode = prev.next;
  12. prev.next = delNode.next;
  13. delNode.next = null;
  14. size--;
  15. return (E) delNode.e;
  16. }

根据上面的方法,可以很快的实现removeFirst()和removeLast()方法

  1. public E removeFirst() {
  2. return remove(0);
  3. }
  4. public E removeLast() {
  5. return remove(size - 1);
  6. }

toString()

  1. @Override
  2. public String toString() {
  3. StringBuilder res = new StringBuilder();
  4. Node cur = dummyHead.next;
  5. //你可以使用上面的for循环
  6. while (cur != null) {
  7. res.append(cur + "->");
  8. cur = cur.next;
  9. }
  10. res.append("NULL");
  11. return res.toString();
  12. }

全部代码

  1. public class LinkedList<E> {
  2. private class Node<E> {
  3. public E e;
  4. public Node next;
  5. public Node(E e, Node next) {
  6. this.e = e;
  7. this.next = next;
  8. }
  9. public Node(E e) {
  10. this(e, null);
  11. }
  12. public Node() {
  13. this(null, null);
  14. }
  15. @Override
  16. public String toString() {
  17. return e.toString();
  18. }
  19. }
  20. private Node dummyHead;
  21. private int size;
  22. public LinkedList() {
  23. dummyHead = new Node(null, null);
  24. size = 0;
  25. }
  26. public int getSize() {
  27. return size;
  28. }
  29. public boolean isEmpty() {
  30. return size == 0;
  31. }
  32. public void addFirst(E e) {
  33. add(0,e);
  34. }
  35. public void add(int index, E e) {
  36. if (index < 0 || index > size) {
  37. throw new IllegalArgumentException("参数错误");
  38. }
  39. Node prev = dummyHead;
  40. for (int i = 0; i < index; i++) {
  41. prev = prev.next;
  42. }
  43. prev.next = new Node(e, prev.next);
  44. size++;
  45. }
  46. public void addLast(E e) {
  47. add(size, e);
  48. }
  49. public E get(int index) {
  50. if (index < 0 || index >= size) {
  51. throw new IllegalArgumentException("参数错误");
  52. }
  53. Node cur = dummyHead.next;
  54. for (int i = 0; i < index; i++) {
  55. cur = cur.next;
  56. }
  57. return (E) cur.e;
  58. }
  59. public E getFirst() {
  60. return get(0);
  61. }
  62. public E getLast() {
  63. return get(size - 1);
  64. }
  65. public void set(int index, E e) {
  66. if (index < 0 || index >= size) {
  67. throw new IllegalArgumentException("参数错误");
  68. }
  69. Node cur = dummyHead.next;
  70. for (int i = 0; i < index; i++) {
  71. cur = cur.next;
  72. }
  73. cur.e = e;
  74. }
  75. public boolean contains(E e) {
  76. for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
  77. if (cur.e.equals(e)) {
  78. return true;
  79. }
  80. }
  81. return false;
  82. }
  83. public E remove(int index) {
  84. if (index < 0 || index >= size) {
  85. throw new IllegalArgumentException("参数错误");
  86. }
  87. Node prev = dummyHead;
  88. for (int i = 0; i < index; i++) {
  89. prev = prev.next;
  90. }
  91. Node delNode = prev.next;
  92. prev.next = delNode.next;
  93. delNode.next = null;
  94. size--;
  95. return (E) delNode.e;
  96. }
  97. public E removeFirst() {
  98. return remove(0);
  99. }
  100. public E removeLast() {
  101. return remove(size - 1);
  102. }
  103. @Override
  104. public String toString() {
  105. StringBuilder res = new StringBuilder();
  106. Node cur = dummyHead.next;
  107. while (cur != null) {
  108. res.append(cur + "->");
  109. cur = cur.next;
  110. }
  111. res.append("NULL");
  112. return res.toString();
  113. }
  114. }

使用链表实现栈

由于链表的addFirst()和removeFirst()的操作都是O(1),所以我们使用链表头作为栈顶,具体的实现逻辑如下

  1. public class LinkedListStack<E> implements Stack<E> {
  2. private LinkedList<E> linkedList;
  3. public LinkedListStack() {
  4. linkedList = new LinkedList<>();
  5. }
  6. @Override
  7. public void push(E e) {
  8. linkedList.addFirst(e);
  9. }
  10. @Override
  11. public E pop() {
  12. return linkedList.removeFirst();
  13. }
  14. @Override
  15. public boolean isEmpty() {
  16. return linkedList.isEmpty();
  17. }
  18. @Override
  19. public int getSize() {
  20. return linkedList.getSize();
  21. }
  22. @Override
  23. public E peek() {
  24. return linkedList.getFirst();
  25. }
  26. @Override
  27. public String toString() {
  28. StringBuilder res = new StringBuilder();
  29. res.append("Stack: top ");
  30. res.append(linkedList);
  31. return res.toString();
  32. }
  33. }

使用链表实现队列

我们之前使用数组实现队列,由于它的dequeue操作是O(n)级别的,所以我们使用front来标记队首,使用循环队列设计,同样的在链表中从链表尾部删除或增加元素都是O(n)级别的,为了解决这一个问题,我们决定在链表的尾部增加一个tail变量来标记,从而使得在尾部增加元素是O(1)级别的
链表 - 图7
另外考虑在尾部删除一个元素是O(1)的吗? 答案是不是。因为我们删除一个节点需要知道该节点的前一个节点,而知道tail节点是无法知道tail的前一个节点的,我们还是要遍历。所以我们在head端删除元素,在tail端添加元素,并且由于只涉及到头部和尾部的操作,所以我们也不需要添加虚拟头结点了
链表 - 图8
下面就是实现的代码

  1. public class LinkedListQueue<E> implements Queue<E> {
  2. private class Node<E> {
  3. public E e;
  4. public Node next;
  5. public Node(E e, Node next) {
  6. this.e = e;
  7. this.next = next;
  8. }
  9. public Node(E e) {
  10. this(e, null);
  11. }
  12. public Node() {
  13. this(null, null);
  14. }
  15. @Override
  16. public String toString() {
  17. return e.toString();
  18. }
  19. }
  20. private Node head;
  21. private Node tail;
  22. private int size;
  23. public LinkedListQueue() {
  24. head = null;
  25. tail = null;
  26. size = 0;
  27. }
  28. @Override
  29. public void enqueue(E e) {
  30. //队列为空时,tail和head都为null 添加元素后二者都指向第一个元素
  31. if (size == 0) {
  32. tail = new Node(e);
  33. head = tail;
  34. } else {
  35. tail.next = new Node(e);
  36. tail = tail.next;
  37. }
  38. size++;
  39. }
  40. @Override
  41. public E dequeue() {
  42. if (size == 0) {
  43. throw new IllegalArgumentException("队列为空");
  44. }
  45. Node delNode = head;
  46. head = head.next;
  47. delNode.next = null;
  48. size--;
  49. //如果队列为空了,此时tail指向的是delNode,此时应该让tail为null
  50. if (size == 0) {
  51. tail = null;
  52. }
  53. return (E) delNode.e;
  54. }
  55. @Override
  56. public E getFront() {
  57. if (head == null) {
  58. throw new IllegalArgumentException("队列为空");
  59. }
  60. return (E) head.e;
  61. }
  62. @Override
  63. public int getSize() {
  64. return size;
  65. }
  66. @Override
  67. public boolean isEmpty() {
  68. return size == 0;
  69. }
  70. @Override
  71. public String toString() {
  72. StringBuilder res = new StringBuilder();
  73. res.append("Queue: front ");
  74. Node cur = head;
  75. while (cur != null) {
  76. res.append(cur + "->");
  77. cur = cur.next;
  78. }
  79. res.append("NULL tail");
  80. return res.toString();
  81. }
  82. }