1、結合之前實現的鏈表這個數據結構,如果只對鏈表的頭部進行增加和刪除,時間復雜度是O(1)的,只對鏈表的頭部進行查詢的話,時間復雜度是O(1)的。那么,滿足這樣的數據結構是什么呢,就是棧,棧這種數據結構是后入先出的,或者先進后出的,只對棧的一端,就是棧頂進行操作,無論是添加元素、刪除元素、查詢元素,都是在棧頂進行的。所以對於鏈表來說,可以將鏈表的頭部當作棧頂,用鏈表做為棧的底層實現來實現一個棧。
創建一個棧的接口,可以使用數組的方式或者鏈表的方式進行實現棧的功能哦!
1 package com.stack; 2 3 /** 4 * @param <E> 使用泛型,可以接受任何數據類型的 5 */ 6 public interface Stack<E> { 7 8 /** 9 * 獲取到棧里面的大小 10 * 11 * @return 12 */ 13 public int getSize(); 14 15 /** 16 * 判斷棧是否為空 17 * 18 * @return 19 */ 20 public boolean isEmpty(); 21 22 /** 23 * 向棧中添加一個元素,即入棧 24 * 25 * @param e 26 */ 27 public void push(E e); 28 29 /** 30 * 從棧中取出棧頂的元素,即出棧 31 * 32 * @return 33 */ 34 public E pop(); 35 36 /** 37 * 查看棧頂的元素 38 * 39 * @return 40 */ 41 public E peek(); 42 43 }
由於之前已經使用過數組實現棧,所以這里使用的是鏈表實現棧的功能,具體代碼,如下所示:
1 package com.linkedlist; 2 3 import com.stack.Stack; 4 5 /** 6 * 7 */ 8 public class LinkedListStack<E> implements Stack<E> { 9 10 // 自己實現的LinkedList鏈表這種數據結構,私有的鏈表類對象。 11 private LinkedList<E> linkedList; 12 13 /** 14 * 構造函數,創建一個LinkedList對象 15 */ 16 public LinkedListStack() { 17 linkedList = new LinkedList<>(); 18 } 19 20 /** 21 * 返回鏈表實現的棧的大小 22 * 23 * @return 24 */ 25 @Override 26 public int getSize() { 27 return linkedList.getSize(); 28 } 29 30 /** 31 * 返回鏈表實現的棧是否為空 32 * 33 * @return 34 */ 35 @Override 36 public boolean isEmpty() { 37 return linkedList.isEmpty(); 38 } 39 40 /** 41 * 向鏈表實現的棧中添加元素,即入棧操作,將元素添加到棧的棧頂 42 * 43 * @param e 44 */ 45 @Override 46 public void push(E e) { 47 // 對鏈表的頭部進行操作,時間復雜度是O(1) 48 linkedList.addFirst(e); 49 } 50 51 /** 52 * 從鏈表實現的棧中刪除元素,即出棧操作 53 * 54 * @return 55 */ 56 @Override 57 public E pop() { 58 // 對鏈表的頭部取出元素操作,時間復雜度是O(1) 59 return linkedList.removeFirst(); 60 } 61 62 /** 63 * 查看棧頂的元素 64 * 65 * @return 66 */ 67 @Override 68 public E peek() { 69 // 對鏈表的頭部查看元素操作,時間復雜度是O(1) 70 return linkedList.getFirst(); 71 } 72 73 @Override 74 public String toString() { 75 StringBuilder stringBuilder = new StringBuilder(); 76 // 鏈表的左側是棧頂哦! 77 stringBuilder.append("Stack:top "); 78 stringBuilder.append(linkedList); 79 return stringBuilder.toString(); 80 } 81 82 public static void main(String[] args) { 83 LinkedListStack<Integer> linkedListStack = new LinkedListStack<>(); 84 for (int i = 0; i < 10; i++) { 85 // 入棧操作 86 linkedListStack.push(i); 87 System.out.println(linkedListStack.toString()); 88 } 89 90 // 出棧操作 91 linkedListStack.pop(); 92 System.out.println("出棧操作: " + linkedListStack.toString()); 93 94 // 獲取到棧的大小 95 int size = linkedListStack.getSize(); 96 System.out.println("獲取到棧的大小: " + size); 97 98 // 獲取到棧頂的元素內容 99 Integer peek = linkedListStack.peek(); 100 System.out.println("獲取到棧頂的元素內容: " + peek); 101 } 102 103 }
2、數組棧和鏈表棧的復雜度都是一致的,都是O(1)的。即使運行時間略有區別。對數組棧的性能和鏈表棧的性能進行對比分析。
1 package com.main; 2 3 import com.linkedlist.LinkedListStack; 4 import com.queue.ArrayQueue; 5 import com.queue.LoopQueue; 6 import com.queue.Queue; 7 import com.stack.ArrayStack; 8 import com.stack.Stack; 9 10 import java.util.Random; 11 12 /** 13 * 14 */ 15 public class Main { 16 17 /** 18 * 測試使用stack運行opCount個push和pop操作所需要的時間,單位:秒 19 * <p> 20 * 數組棧和鏈表棧的復雜度都是一致的,都是O(1)的。即使運行時間略有區別。 21 * 22 * @param queue 23 * @param opCount 24 * @return 25 */ 26 public static double queuePerforms(Stack<Integer> queue, int opCount) { 27 // 開始時間 28 long startTime = System.nanoTime(); 29 30 Random random = new Random(); 31 // 入隊操作 32 for (int i = 0; i < opCount; i++) { 33 queue.push(random.nextInt(Integer.MAX_VALUE)); 34 } 35 36 // 出隊操作 37 for (int i = 0; i < opCount; i++) { 38 queue.pop(); 39 } 40 41 // 結束時間 42 long endTime = System.nanoTime(); 43 44 // 秒與納秒之前差九位數 45 return (endTime - startTime) / 1000000000.0; 46 } 47 48 public static void main(String[] args) { 49 int opCount = 100000;// 十萬次 50 51 // 數組棧的性能 52 // 對於數組棧來說,需要重新分配靜態數組,將原來的數組拷貝到新的數組中,即擴容操作。 53 ArrayStack<Integer> arrayStack = new ArrayStack<>(); 54 double time1 = queuePerforms(arrayStack, opCount); 55 System.out.println("ArrayStack, time: " + time1 + " s"); 56 57 58 // 鏈表棧的性能 59 // 其實這個時間比較復雜,因為LinkedListStack中包含更多的new操作。 60 LinkedListStack<Integer> linkedListStack = new LinkedListStack<>(); 61 double time2 = queuePerforms(linkedListStack, opCount); 62 System.out.println("LinkedListStack, time: " + time2 + " s"); 63 } 64 65 }
3、帶有尾指針的鏈表,使用鏈表實現隊列。
1)、結合之前實現的鏈表這個數據結構,如果只對鏈表的頭部進行增加和刪除,時間復雜度是O(1)的,只對鏈表的頭部進行查詢的話,時間復雜度是O(1)的。如果對鏈表的尾部進行操作的話,無論是添加元素還是刪除元素,時間復雜度都是O(n)的。對於隊列這種數據結構來說,需要在這種線性結構中的一端插入元素,在另外一端刪除元素,所以我們勢必會在這種線性結構的兩端同時操作,那么此時就會有一端的操作,它的復雜度是O(n)級別的。
2)、對於使用數組來實現隊列的時候,也遇到類似問題,需要改進數組實現隊列的方式,所以產生了循環隊列,對於鏈表也存在同樣的問題,我們不能直接使用之前的鏈表結構,需要引入改進該鏈表,由此引入了尾指針。改進思路,對於鏈表來說,在鏈表的頭部的位置,不管插入元素還是刪除元素,都十分容易,因為對於鏈表來說,有head這個變量來幫助我們標記鏈表的頭部在哪里,擁有了這個標記以后,在鏈表的頭部進行添加元素還是刪除元素都是容易的。
3)、所以在鏈表的尾部,再創建一個Node類型的變量tail,來標記鏈表的尾部在哪里,所以,如果知道了鏈表的尾部,再添加一個元素會是非常容易的。這就相當於給鏈表添加元素,在鏈表索引為size的位置添加一個元素,換句話說,tail這個位置的節點就是我們待添加元素的位置,也就是最后一個位置之前的那個節點,相應的,我們在tail這個節點的后面添加一個節點是非常容易的。
4)、所以,對於鏈表來說,在head端和tail端添加節點都是非常容易的。
3.1、考慮,如何在tail端刪除一個節點。鏈表新增尾指針,使用鏈表實現隊列。
1)、如何在tail端刪除一個節點,注意的是,鏈表的結構不是對稱的,之前的鏈表結構中,想要刪除一個元素,需要找到待刪除元素的前一個位置的節點,如果現在想要刪除tail這個位置所指向的節點,就要找到tail這個位置它前一個位置的節點,如何找到tail這個節點之前的那個位置節點呢,對於此時的鏈表來說,只有一個next指向后一個節點,所以此時是無法使用O(1)的復雜度直接找到tail這個節點它之前那個位置的節點的,此時,還是需要從head頭部節點循環遍歷,這樣時間復雜度變成了O(n),所以,即使在鏈表的尾部標記了tail,我們還是無法使用O(1)的復雜度直接刪除tail這個位置的節點的。即從tail刪除元素不容易的。
2)、但是,對於,在鏈表的頭部head這個節點刪除或者新增一個節點非常容易的,所以根據此分析,改進鏈表,即在鏈表的尾部添加一個Node節點tail,來標記整個鏈表中尾節點處在什么位置,然后在在鏈表的頭部head這個節點刪除或者新增一個節點非常容易的,對於tail端,我們只是添加元素容易,刪除元素不容易。
3)、對於隊列中,隊首和隊尾的選擇,由於tail端刪除元素不容易,只能從tail端插入元素,從head端刪除元素,所以,刪除元素的這一端在隊列中稱為隊首,負責出隊,添加元素的這一端稱為隊尾,負責入隊。
4)、注意,由於對這個鏈表的操作全都在鏈表的一側完成,也就是head端或者tail端完成,就不使用虛擬頭節點了,因為不牽扯到對鏈表的中間的一個元素進行插入或者刪除這樣的操作,所以也就沒有必要統一對鏈表的中間元素進行操作和對鏈表兩側的元素進行操作,他們之間可能帶來的邏輯不統一的問題。
如果添加了tail這個Node節點以后,還需要注意一個情況,就是此時,當我們的鏈表為空的時候。head和tail都將指向空,由於沒有虛擬頭節點,要注意鏈表為空的情況。
鏈表新增tail節點,結合head頭部節點的鏈表實現隊列的功能。
1 package com.queue; 2 3 /** 4 * 使用鏈表創建隊列 5 * 6 * @param <E> 7 */ 8 public class LinkedListQueue<E> implements Queue<E> { 9 10 // 由於LinkedListQueue也是一個鏈表,所以需要鏈表的節點Node 11 // 鏈表是由一個一個節點組成 12 private class Node { 13 // 設置公有的,可以讓外部類進行修改和設置值 14 public E e;// 成員變量e存放元素 15 public Node next;// 成員變量next指向下一個節點,指向Node的一個引用 16 17 /** 18 * 含參構造函數 19 * 20 * @param e 21 * @param next 22 */ 23 public Node(E e, Node next) { 24 this.e = e; 25 this.next = next; 26 } 27 28 /** 29 * 無參構造函數 30 */ 31 public Node() { 32 this(null, null); 33 } 34 35 /** 36 * 如果用戶只傳了e,那么可以調用含參構造函數,將next指定為null 37 * 38 * @param e 39 */ 40 public Node(E e) { 41 this(e, null); 42 } 43 44 @Override 45 public String toString() { 46 return e.toString(); 47 } 48 } 49 50 51 // 創建頭節點和尾節點 52 private Node head;// 鏈表的頭節點 53 private Node tail;// 鏈表的尾節點 54 private int size;// 鏈表的長度大小 55 56 /** 57 * 無參構造函數,和默認的構造函數做的是一樣的。 58 * 鏈表初始化的時候head、tail都是空 59 */ 60 public LinkedListQueue() { 61 head = null; 62 tail = null; 63 size = 0; 64 } 65 66 /** 67 * 使用鏈表實現的隊列的,入隊操作。 68 * <p> 69 * 鏈表實現的入隊操作,是從鏈表的尾部進行。 70 * <p> 71 * 此方法是O(1)級別的復雜度。 72 * 73 * @param e 74 */ 75 @Override 76 public void enqueue(E e) { 77 // 首先判斷鏈表的尾部是否為空,如果tail為空,說明head也是空的,此時鏈表是空的 78 // 但凡鏈表包含元素,tail指向的是尾節點位置,就不會是空的。 79 if (tail == null) { 80 // 如果尾部節點是空,就直接在尾部插入一個系節點,此節點存儲e元素 81 tail = new Node(e); 82 // 此時,莫要忘記維護head,此時head==tail 83 head = tail; 84 } else { 85 // 如果tail指向的位置不為空,那么在tail的next位置,創建新節點,存儲元素e 86 tail.next = new Node(e); 87 // 然后維護tail的位置即可,讓tail指向鏈表的最后一個位置的元素。 88 // 新的tail等於剛剛創建的tail的next。此時就不用維護head頭部節點的位置了。 89 tail = tail.next; 90 } 91 92 // 最后,維護size的大小即可 93 size++; 94 } 95 96 /** 97 * 使用鏈表實現的隊列的,出隊操作。 98 * <p> 99 * 此方法是O(1)級別的復雜度。不需要遍歷隊列所以元素 100 * 101 * @return 102 */ 103 @Override 104 public E dequeue() { 105 // 出隊過程,首先,需要判斷隊列能否可以出隊一個元素。 106 // 換句話說,如果此時隊列為空的時候,就無法出隊,拋出異常即可。 107 if (isEmpty()) { 108 throw new IllegalArgumentException("Cannot dequeue from an empty queue."); 109 } 110 111 // 如果有元素,就進行出隊操作。 112 // 此操作類似於從鏈表的頭部刪除一個元素。 113 // 首先,returnNode出隊元素所在的節點應該就是head這個位置所指向的節點。 114 Node returnNode = head; 115 // 讓head指向原來head的next下一個位置節點。新的head跳過returnNode,直接指向head.next。 116 head = head.next; 117 118 // 這樣一來returnNode的next其實相當於指向了head這個節點。 119 // 此時讓returnNode從鏈表中斷開,需要操作的是returnNode.next = null; 120 returnNode.next = null; 121 122 // 此時,需要注意,當指向了head = head.next的時候,也就是我們的那個head這個節點指向了原來 123 // 我們頭節點的下一個節點,但是這個下一個節點可能是空的,也即是說,我們的鏈表只有一個元素。 124 // 我們把returnNode出隊之后,我們的鏈表就為空了。此時需要判斷head是否為空,然后維護tail這個節點。 125 if (head == null) { 126 // 此時需要判斷head是否為空,然后維護tail這個節點。讓tail節點為空。 127 // 例如,只有一個元素,head和tail都指向這個元素,如果經過上面的出隊操作之后, 128 // 如果此時tail還指向此元素,就錯了,所以需要維護tail的節點為空即可。 129 tail = null; 130 } 131 132 // 維護size的大小 133 size--; 134 135 // 返回出隊的節點元素 136 return returnNode.e; 137 } 138 139 /** 140 * 使用鏈表實現的隊列的,獲取到隊首元素。 141 * 142 * @return 143 */ 144 @Override 145 public E getFront() { 146 // 獲取隊首元素 147 if (isEmpty()) { 148 throw new IllegalArgumentException("Queue is empty."); 149 } 150 151 // 否則返回隊首的元素即可 152 return head.e; 153 } 154 155 /** 156 * 返回鏈表的大小 157 * 158 * @return 159 */ 160 @Override 161 public int getSize() { 162 return size; 163 } 164 165 /** 166 * 判斷鏈表是否為空 167 * 168 * @return 169 */ 170 @Override 171 public boolean isEmpty() { 172 return size == 0; 173 } 174 175 @Override 176 public String toString() { 177 StringBuilder stringBuilder = new StringBuilder(); 178 // 隊首,負責出隊 179 stringBuilder.append("Queue : front "); 180 // 使用while循環進行循環 181 Node current = head; 182 while (current != null) { 183 stringBuilder.append(current + "->"); 184 current = current.next; 185 } 186 // 隊尾,負責入隊 187 stringBuilder.append("NULL tail "); 188 return stringBuilder.toString(); 189 } 190 191 public static void main(String[] args) { 192 // 基於鏈表實現的隊列 193 LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>(); 194 // 隊列的入隊操作 195 for (int i = 0; i < 10; i++) { 196 linkedListQueue.enqueue(i); 197 // System.out.println("隊列的入隊操作: " + linkedListQueue); 198 System.out.println(linkedListQueue); 199 200 // if (i % 3 == 2) { 201 // linkedListQueue.dequeue(); 202 // System.out.println(linkedListQueue); 203 // } 204 } 205 206 // 隊列的出隊操作 207 linkedListQueue.dequeue(); 208 System.out.println("隊列的出隊操作: " + linkedListQueue.toString()); 209 210 // 獲取到隊首的元素 211 Integer front = linkedListQueue.getFront(); 212 System.out.println("獲取到隊首的元素: " + front); 213 } 214 215 }
4、數組隊列,循環數組隊列,鏈表隊列的性能測試比較。如下所示:
1 package com.queue; 2 3 import java.util.Random; 4 5 /** 6 * 7 */ 8 public class Main { 9 10 /** 11 * 測試使用queue運行opCount個enqueue和dequeue操作所需要的時間,單位:秒 12 * 13 * @param queue 14 * @param opCount 15 * @return 16 */ 17 public static double queuePerforms(Queue<Integer> queue, int opCount) { 18 // 開始時間 19 long startTime = System.nanoTime(); 20 21 Random random = new Random(); 22 // 入隊操作 23 for (int i = 0; i < opCount; i++) { 24 queue.enqueue(random.nextInt(Integer.MAX_VALUE)); 25 } 26 27 // 出隊操作 28 for (int i = 0; i < opCount; i++) { 29 queue.dequeue(); 30 } 31 32 // 結束時間 33 long endTime = System.nanoTime(); 34 35 // 秒與納秒之前差九位數 36 return (endTime - startTime) / 1000000000.0; 37 } 38 39 public static void main(String[] args) { 40 int opCount = 100000;// 十萬次 41 42 // 數組隊列的性能 43 // 影響數組隊列性能的是出隊操作,因為每一個元素都要進行前移操作 44 // 時間復雜度是O(n*n)。 45 ArrayQueue<Integer> arrayQueue = new ArrayQueue<>(); 46 double time1 = queuePerforms(arrayQueue, opCount); 47 System.out.println("ArrayQueue, time: " + time1 + " s"); 48 49 50 // 循環隊列的性能 51 // 時間復雜度是O(1)。 52 LoopQueue<Integer> loopQueue = new LoopQueue<>(); 53 double time2 = queuePerforms(loopQueue, opCount); 54 System.out.println("LoopQueue, time: " + time2 + " s"); 55 56 57 // 鏈表隊列的性能。循環隊列和鏈表隊列的時間復雜度是一樣的。 58 // 時間復雜度是O(1)。 59 LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>(); 60 double time3 = queuePerforms(linkedListQueue, opCount); 61 System.out.println("LinkedListQueue, time: " + time3 + " s"); 62 } 63 64 }
作者:別先生
博客園:https://www.cnblogs.com/biehongli/
如果您想及時得到個人撰寫文章以及著作的消息推送,可以掃描上方二維碼,關注個人公眾號哦。