項目主頁: https://github.com/gozhuyinglong/blog-demos
本文源碼: https://github.com/gozhuyinglong/blog-demos/tree/main/java-data-structures/src/main/java/com/github/gozhuyinglong/datastructures/linkedlist
經過前篇文章《數組》瞭解到數組的存儲結構是一塊連續的內存,插入和刪除元素時其每一個部分都有可能總體移動。爲了不這樣的線性開銷,咱們須要保證數據能夠不連續存儲。本篇介紹另外一種數據結構:鏈表。java
鏈表是一種線性的數據結構,其物理存儲結構是零散的,數據元素經過指針實現鏈表的邏輯順序。鏈表由一系列結點(鏈表中每個元素稱爲節點)組成,節點能夠在內存中動態生成。node
鏈表的特性:git
鏈表分爲單鏈表、雙鏈表和環形鏈表,下面經過實例逐個介紹。github
單鏈表又叫單向鏈表,其節點由兩部分構成:算法
data
域:數據域,用來存儲元素數據next
域:用於指向下一節點單鏈表的結構以下圖:編程
單鏈表的全部操做都是從head
開始,head
自己不存儲元素,其next
指向第一個節點,而後順着next
鏈進行一步步操做。其尾部節點的next
指向爲空,這也是判斷尾部節點的依據。數組
這裏主要介紹插入和刪除節點的操做。數據結構
向單鏈表中插入一個新節點,能夠經過調整兩次next
指向來完成。以下圖所示,X爲新節點,將其next
指向爲A2,再將A1的next
指向爲X便可。ide
如果從尾部節點插入,直接將尾部節點的next
指向新節點便可。this
從單鏈表中刪除一個節點,能夠經過修改next
指向來實現,以下圖所示,將A1的next
指向爲A3,這樣便刪除A2,A2的內存空間會自動被垃圾回收。
如果刪除尾部節點,直接將上一節點的next
指向爲空便可。
咱們使用Java代碼來實現一個單鏈表。其中Node
類存儲單鏈表的一個節點,SinglyLinkedList
類實現了單鏈表的全部操做方法。SinglyLinkedList
類使用帶頭節點的方式實現,即head
節點,該節點不存儲數據,只是標記單鏈表的開始。
public class SinglyLinkedListDemo { public static void main(String[] args) { Node node1 = new Node(1, "張三"); Node node2 = new Node(3, "李四"); Node node3 = new Node(7, "王五"); Node node4 = new Node(5, "趙六"); SinglyLinkedList singlyLinkedList = new SinglyLinkedList(); System.out.println("-----------添加節點(尾部)"); singlyLinkedList.add(node1); singlyLinkedList.add(node2); singlyLinkedList.add(node3); singlyLinkedList.add(node4); singlyLinkedList.print(); System.out.println("-----------獲取某個節點"); Node node = singlyLinkedList.get(3); System.out.println(node); singlyLinkedList.remove(node3); System.out.println("-----------移除節點"); singlyLinkedList.print(); System.out.println("-----------修改節點"); singlyLinkedList.update(new Node(5, "趙六2")); singlyLinkedList.print(); System.out.println("-----------按順序添加節點"); Node node5 = new Node(4, "王朝"); singlyLinkedList.addOfOrder(node5); singlyLinkedList.print(); } private static class SinglyLinkedList { // head節點是單鏈表的開始,不用來存儲數據 private Node head = new Node(0, null); /** * 將節點添加到尾部 * * @param node */ public void add(Node node) { Node temp = head; while (true) { if (temp.next == null) { temp.next = node; break; } temp = temp.next; } } /** * 按順序添加節點 * * @param node */ public void addOfOrder(Node node) { Node temp = head; while (true) { if (temp.next == null) { temp.next = node; break; } else if(temp.next.key > node.getKey()){ node.next = temp.next; temp.next = node; break; } temp = temp.next; } } /** * 獲取某個節點 * * @param key * @return */ public Node get(int key) { if (head.next == null) { return null; } Node temp = head.next; while (temp != null) { if (temp.key == key) { return temp; } temp = temp.next; } return null; } /** * 移除一個節點 * * @param node */ public void remove(Node node) { Node temp = head; while (true) { if (temp.next == null) { break; } if (temp.next.key == node.key) { temp.next = temp.next.next; break; } temp = temp.next; } } /** * 修改一個節點 * * @param node */ public void update(Node node) { Node temp = head.next; while (true) { if (temp == null) { break; } if (temp.key == node.key) { temp.value = node.value; break; } temp = temp.next; } } /** * 打印鏈表 */ public void print() { Node temp = head.next; while (temp != null) { System.out.println(temp.toString()); temp = temp.next; } } } private static class Node { private final int key; private String value; private Node next; public Node(int key, String value) { this.key = key; this.value = value; } public int getKey() { return key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public Node getNext() { return next; } @Override public String toString() { return "Node{" + "key=" + key + ", value='" + value + '\'' + '}'; } } }
輸出結果:
-----------添加節點(尾部) Node{key=1, value='張三'} Node{key=3, value='李四'} Node{key=7, value='王五'} Node{key=5, value='趙六'} -----------獲取某個節點 Node{key=3, value='李四'} -----------移除節點 Node{key=1, value='張三'} Node{key=3, value='李四'} Node{key=5, value='趙六'} -----------修改節點 Node{key=1, value='張三'} Node{key=3, value='李四'} Node{key=5, value='趙六2'} -----------按順序添加節點 Node{key=1, value='張三'} Node{key=3, value='李四'} Node{key=4, value='王朝'} Node{key=5, value='趙六2'}
經過對單鏈表的分析,能夠看出單鏈表有以下缺點:
(1)單鏈表的查找方法只能是一個方向
(2)單鏈表不能自我刪除,須要靠上一節點進行輔助操做。
而這些缺點能夠經過雙鏈表來解決,下面來看詳細介紹。
雙鏈表又叫雙向鏈表,其節點由三部分構成:
prev
域:用於指向上一節點data
域:數據域,用來存儲元素數據next
域:用於指向下一節點雙鏈表的結構以下圖:
雙鏈表的操做能夠從兩端開始,從第一個節點經過next
指向能夠一步步操做到尾部,從最後一個節點經過prev
指向能夠一步步操做到頭部。
這裏主要介紹插入和刪除節點的操做。
向雙鏈表中插入一個新節點,須要經過調整兩次prev
指向和兩次next
指向來完成。以下圖所示,X爲新節點,將A1的next
指向X,將X的next
指向A2,將A2的prev
指向X,將X的prev
指向A1便可。
從雙鏈表中刪除一個節點,須要經過調整一次prev
指向和一次next
指向來完成。以下圖所示,刪除A2節點,將A1的next
指向A3,將A3的 prev
指向A1便可。
咱們使用Java代碼來實現一個雙鏈表。其中 Node
類存儲雙鏈表的一個節點,DoublyLinkedListDemo
類實現雙鏈表的全部操做方法。DoublyLinkedListDemo
類使用不帶頭節點的方式實現,其中first
爲第一個節點,last
爲最後一個節點。這兩個節點默認都爲空,若只有一個元素時,則兩個節點指向同一元素。
public class DoublyLinkedListDemo { public static void main(String[] args) { DoublyLinkedList doublyLinkedList = new DoublyLinkedList(); System.out.println("-----------從尾部添加節點"); doublyLinkedList .addToTail(new Node(1, "張三")) .addToTail(new Node(3, "李四")) .addToTail(new Node(7, "王五")) .addToTail(new Node(5, "趙六")) .print(); System.out.println("-----------從頭部添加節點"); doublyLinkedList .addToHead(new Node(0, "朱開山")) .print(); System.out.println("-----------獲取某個節點"); System.out.println(doublyLinkedList.get(3)); System.out.println("-----------移除節點"); doublyLinkedList .remove(new Node(3, "李四")) .print(); System.out.println("-----------修改節點"); doublyLinkedList .update(new Node(5, "趙六2")).print(); System.out.println("-----------按順序添加節點"); doublyLinkedList .addOfOrder(new Node(4, "王朝")) .print(); } private static class DoublyLinkedList { private Node first = null; // first節點是雙鏈表的頭部,即第一個節點 private Node last = null; // tail節點是雙鏈表的尾部,即最後一個節點 /** * 從尾部添加 * * @param node */ public DoublyLinkedList addToTail(Node node) { if (last == null) { first = node; } else { last.next = node; node.prev = last; } last = node; return this; } /** * 按照順序添加 * * @param node */ public DoublyLinkedList addOfOrder(Node node) { if (first == null) { first = node; last = node; return this; } // node比頭節點小,將node設爲頭節點 if (first.key > node.key) { first.prev = node; node.next = first; first = node; return this; } // node比尾節點大,將node設爲尾節點 if (last.key < node.key) { last.next = node; node.prev = last; last = node; return this; } Node temp = first.next; while (true) { if (temp.key > node.key) { node.next = temp; node.prev = temp.prev; temp.prev.next = node; temp.prev = node; break; } temp = temp.next; } return this; } /** * 從頭部添加 * * @param node */ public DoublyLinkedList addToHead(Node node) { if (first == null) { last = node; } else { node.next = first; first.prev = node; } first = node; return this; } /** * 獲取節點 * * @param key * @return */ public Node get(int key) { if (first == null) { return null; } Node temp = first; while (temp != null) { if (temp.key == key) { return temp; } temp = temp.next; } return null; } /** * 移除節點 * * @param node */ public DoublyLinkedList remove(Node node) { if (first == null) { return this; } // 要移除的是頭節點 if (first == node) { first.next.prev = null; first = first.next; return this; } // 要移除的是尾節點 if (last == node) { last.prev.next = null; last = last.prev; return this; } Node temp = first.next; while (temp != null) { if (temp.key == node.key) { temp.prev.next = temp.next; temp.next.prev = temp.prev; break; } temp = temp.next; } return this; } /** * 修改某個節點 * * @param node */ public DoublyLinkedList update(Node node) { if (first == null) { return this; } Node temp = first; while (temp != null) { if (temp.key == node.key) { temp.value = node.value; break; } temp = temp.next; } return this; } /** * 打印鏈表 */ public void print() { if (first == null) { return; } Node temp = first; while (temp != null) { System.out.println(temp); temp = temp.next; } } } private static class Node { private final int key; private String value; private Node prev; // 指向上一節點 private Node next; // 指向下一節點 public Node(int key, String value) { this.key = key; this.value = value; } @Override public String toString() { return "Node{" + "key=" + key + ", value='" + value + '\'' + '}'; } } }
輸出結果:
-----------從尾部添加節點 Node{key=1, value='張三'} Node{key=3, value='李四'} Node{key=7, value='王五'} Node{key=5, value='趙六'} -----------從頭部添加節點 Node{key=0, value='朱開山'} Node{key=1, value='張三'} Node{key=3, value='李四'} Node{key=7, value='王五'} Node{key=5, value='趙六'} -----------獲取某個節點 Node{key=3, value='李四'} -----------移除節點 Node{key=0, value='朱開山'} Node{key=1, value='張三'} Node{key=7, value='王五'} Node{key=5, value='趙六'} -----------修改節點 Node{key=0, value='朱開山'} Node{key=1, value='張三'} Node{key=7, value='王五'} Node{key=5, value='趙六2'} -----------按順序添加節點 Node{key=0, value='朱開山'} Node{key=1, value='張三'} Node{key=4, value='王朝'} Node{key=7, value='王五'} Node{key=5, value='趙六2'}
環形鏈表又叫循環鏈表,本文講述的是環形單向鏈表,其與單鏈表的惟一區別是尾部節點的next
再也不爲空,則是指向了頭部節點,這樣便造成了一個環。
環形鏈表的結構以下圖:
約瑟夫問題:有時也稱爲約瑟夫斯置換,是一個計算機科學和數學中的問題。在計算機編程的算法中,相似問題又稱爲約瑟夫環。又稱「丟手絹問題」。
引自百度百科:
聽說著名猶太曆史學家Josephus有過如下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,因而決定了一個自殺方式,41我的排成一個圓圈,由第1我的開始報數,每報數到第3人該人就必須自殺,而後再由下一個從新報數,直到全部人都自殺身亡爲止。然而Josephus 和他的朋友並不想聽從。首先從一我的開始,越過k-2我的(由於第一我的已經被越過),並殺掉第 k我的。接着,再越過k-1我的,並殺掉第 k我的。這個過程沿着圓圈一直進行,直到最終只剩下一我的留下,這我的就能夠繼續活着。問題是,給定了和,一開始要站在什麼地方纔能避免被處決。Josephus要他的朋友先僞裝聽從,他將朋友與本身安排在第16個與第31個位置,因而逃過了這場死亡遊戲。17世紀的法國數學家加斯帕在《數目的遊戲問題》中講了這樣一個故事:15個教徒和15 個非教徒在深海上遇險,必須將一半的人投入海中,其他的人才能倖免於難,因而想了一個辦法:30我的圍成一圓圈,從第一我的開始依次報數,每數到第九我的就將他扔入大海,如此循環進行直到僅餘15我的爲止。問怎樣排法,才能使每次投入大海的都是非教徒。
問題分析與算法設計
約瑟夫問題並不難,但求解的方法不少;題目的變化形式也不少。這裏給出一種實現方法。
題目中30我的圍成一圈,於是啓發咱們用一個循環的鏈來表示,可使用結構數組來構成一個循環鏈。結構中有兩個成員,其一爲指向下一我的的指針,以構成環形的鏈;其二爲該人是否被扔下海的標記,爲1表示還在船上。從第一我的開始對還未扔下海的人進行計數,每數到9時,將結構中的標記改成0,表示該人已被扔下海了。這樣循環計數直到有15我的被扔下海爲止。
咱們使用Java代碼來實現一個環形鏈表,並將節點按約瑟夫問題順序出列。
public class CircularLinkedListDemo { public static void main(String[] args) { CircularLinkedList circularLinkedList = new CircularLinkedList(); System.out.println("-----------添加10個節點"); for (int i = 1; i <= 10; i++) { circularLinkedList.add(new Node(i)); } circularLinkedList.print(); System.out.println("-----------按約瑟夫問題順序出列"); circularLinkedList.josephusProblem(3); } private static class CircularLinkedList { private Node first = null; // 頭部節點,即第一個節點 /** * 添加節點,並將新添加的節點的next指向頭部,造成一個環形 * * @param node * @return */ public void add(Node node) { if (first == null) { first = node; first.next = first; return; } Node temp = first; while (true) { if (temp.next == null || temp.next == first) { temp.next = node; node.next = first; break; } temp = temp.next; } } /** * 按約瑟夫問題順序出列 * 即從第1個元素開始報數,報到num時當前元素出列,而後從新從下一個元素開始報數,直至全部元素出列 * * @param num 表示報幾回數 */ public void josephusProblem(int num) { Node currentNode = first; // 將當前節點指向最後一個節點 do { currentNode = currentNode.next; } while (currentNode.next != first); // 開始出列 while (true) { // 當前節點要指向待出列節點的前一節點(雙向環形隊列不須要) for (int i = 0; i < num - 1; i++) { currentNode = currentNode.next; } System.out.printf("%s\t", currentNode.next.no); if(currentNode.next == currentNode){ break; } currentNode.next = currentNode.next.next; } } /** * 輸出節點 */ public void print() { if (first == null) { return; } Node temp = first; while (true) { System.out.printf("%s\t", temp.no); if (temp.next == first) { break; } temp = temp.next; } System.out.println(); } } private static class Node { private final int no; private Node next; // 指向下一節點 public Node(int no) { this.no = no; } @Override public String toString() { return "Node{" + "no=" + no + '}'; } } }
輸出結果:
-----------添加10個節點 1 2 3 4 5 6 7 8 9 10 -----------按約瑟夫問題順序出列 3 6 9 2 7 1 8 5 10 4