0、節點
結點 是數據結構中的基礎,是 構成複雜數據結構的基本組成單位。
public class Node {
public long data;
public Node next;
public Node(long value) {
this.data = value;
}
}
1、鏈表
鏈表:
通常由一連串 節點 組成,每個節點包含任意的實例數據(data fields)和一或兩個用來指向上一個/或下一個節點的位置的鏈接(“links”)。
上面是一個單鏈表的存儲原理圖,head爲頭節點,它不存放任何的數據,只是充當一個指向鏈表中真正存放數據的第一個節點的作用,而每個節點中都有一個 next 引用,指向下一個節點,就這樣一節一節往下面記錄,直到最後一個節點,其中的 next 指向 null 。
2、鏈表的種類和特點
普通鏈表(單鏈表):
節點類保留下一節點的引用。鏈表類只保留頭節點的引用,只能從頭節點插入刪除;
雙端鏈表:
節點類保留下一節點的引用。鏈表類保留頭節點、尾節點的引用,可以從尾節點插入,但只能從頭節點刪除;
雙向鏈表:
節點類保留上一節點、下一節點的引用。鏈表類保留頭節點、尾節點的引用,可以從尾節點插入刪除;
如圖所示:
以上統稱爲 無序鏈表。
無序鏈表 最大特點就是 在頭部或尾部增加 新節點。
有序鏈表:
遞增,遞減或者其他滿足一定條件的規則,插入數據時,從頭結點開始遍歷每個節點,在滿足規則的地方插入新節點。
3、普通鏈表、單鏈表(單端鏈表)
單鏈表 是鏈表中結構最簡單的。一個單鏈表的節點(Node)分爲兩個部分,第一個部分(data)保存或者顯示關於節點的信息,另一個部分存儲下一個節點的地址。最後一個節點存儲地址的部分指向空值。
-
查找:單向鏈表 只可向一個方向遍歷,一般查找一個節點的時候需要從第一個節點開始每次訪問下一個節點,一直訪問到需要的位置。
-
插入:而插入一個節點,對於單向鏈表,我們只提供在鏈表頭插入,只需要將當前插入的節點設置爲頭節點,next指向原頭節點即可。
-
刪除:刪除一個節點,我們將該節點的上一個節點的next指向該節點的下一個節點。
查找圖:
在表頭增加節點:
刪除節點:
package test.node1;
public class Node {
public long data;
public Node next;
public Node(long value) {
this.data = value;
}
//顯示方法
public void display() {
System.out.print(data + " ");
}
}
package test.node1;
/**
* 單鏈表
*/
public class SingleEndLinkList {
/**頭點節*/
private Node first;
public SingleEndLinkList() {
first = null;
}
/**
* 插入節點
* <pre>
* 在頭結點之後插入
* </pre>
*/
public void insertFirst(long value) {
Node aNode = new Node(value);
aNode.next = first;
first = aNode;
}
//刪除頭節點
public Node deleteFirst() {
Node tmp = first.next;
first = tmp;
return tmp;
}
//顯示方法
public void display() {
Node now = first;
while(now != null) {
now.display();
now = now.next;
}
System.out.println();
}
//查找方法
public Node find(long value) {
Node now = first;
while(now.data != value) {
if(now.next == null) {
return null;
}
now = now.next;
}
return now;
}
//根據數值刪除
public Node delete(long value) {
Node now = first;
Node before = first;
while(now.data != value) {
if(now.next == null) {
return null;
}
before = now;
now = now.next;
}
if(now == first) {
first = first.next;
}else {
before.next = now.next;
}
return now;
}
}
4、雙端鏈表
節點類保留下一節點的引用。鏈表類保留頭節點、尾節點的引用,可以從尾節點插入,但只能從頭節點刪除。
package test.node2;
public class Node {
public long data;
public Node next;
public Node(long data) {
this.data = data;
}
// 顯示方法
public void display() {
System.out.print(data + " ");
}
}
package test.node2;
/**
* 雙端鏈表
*/
public class DoubleEndLinkedList {
// 頭節點
private Node first;
//尾節點
private Node last;
public DoubleEndLinkedList() {
first = null;
last = null;
}
//插入節點,在頭結點之後插入
public void insertFirst(long value) {
Node aNode = new Node(value);
if (isEmpty()) {
last = aNode;
}
aNode.next = first;
first = aNode;
}
//尾節點插入
public void insertLast(long value) {
Node aNode = new Node(value);
if (isEmpty()) {
first = aNode;
}
else {
last.next = aNode;
}
last = aNode;
}
//刪除頭節點
public Node deleteFirst() {
Node tmp = first;
if (first.next == null) {
last = null;
}
first = tmp.next;
return tmp;
}
//顯示方法
public void display() {
Node now = first;
while(now != null) {
now.display();
now = now.next;
}
System.out.println();
}
//查找方法
public Node find(long value) {
Node now = first;
while(now.data != value) {
if(now.next == null) {
return null;
}
now = now.next;
}
return now;
}
//根據數值刪除
public Node delete(long value) {
Node now = first;
Node before = first;
while(now.data != value) {
if(now.next == null) {
return null;
}
before = now;
now = now.next;
}
if(now == first) {
first = first.next;
}
else {
before.next = now.next;
}
return now;
}
//判斷是否爲空
public boolean isEmpty() {
return first == null;
}
}
爲什麼不能刪除尾節點? 刪除尾節點時必須知道尾節點的上個節點,但是 雙端鏈表 只能知道下個節點,不知道上個節點,故無法刪除。
5、雙向鏈表
節點類保留上一節點、下一節點的引用。鏈表類保留頭節點、尾節點的引用,可以從尾節點插入刪除。
package test.node3;
public class Node {
// 數據域
public long data;
//指針域(保存下一個節點的引用)
public Node next;
//指針域(保存前一個節點的引用)
public Node previous;
public Node(long value) {
this.data = value;
}
/**
* 顯示方法
*/
public void display() {
System.out.print(data + " ");
}
}
package test.node3;
/**
* 雙向鏈表
*/
public class DoubleByLinkedList {
// 頭結點
private Node first;
//尾結點
private Node last;
public DoubleByLinkedList() {
first = null;
last = null;
}
/**
* 插入一個節點,在頭結點後進行插入
*
* @param value
*/
public void insertFirst(long value) {
Node node = new Node(value);
if (isEmpty()) {
last = node;
} else {
first.previous = node;
}
node.next = first;
first = node;
}
public void insertLast(long value) {
Node node = new Node(value);
if (isEmpty()) {
first = node;
} else {
last.next = node;
node.previous = last;
}
last = node;
}
/**
* 刪除一個結點,在頭結點後進行刪除
*
* @return
*/
public Node deleteFirst() {
Node tmp = first;
if (first.next == null) {
last = null;
} else {
first.next.previous = null;
}
first = tmp.next;
return tmp;
}
/**
* 刪除一個結點,從尾部進行刪除
*
* @return
*/
public Node deleteLast() {
if (first.next == null) {
first = null;
} else {
last.previous.next = null;
}
last = last.previous;
return last;
}
/**
* 顯示方法
*/
public void display() {
Node current = first;
while (current != null) {
current.display();
current = current.next;
}
}
/**
* 查找方法
*
* @param value
* @return
*/
public Node find(long value) {
Node current = first;
while (current.data != value) {
if (current.next == null) {
return null;
}
current = current.next;
}
return current;
}
public Node delete(long value) {
Node current = first;
while (current.data != value) {
if (current.next == null) {
return null;
}
current = current.next;
}
if (current == first) {
first = first.next;
} else {
current.previous.next = current.next;
}
return current;
}
/**
* 判斷是否爲空
*
* @return
*/
public boolean isEmpty() {
return first == null;
}
}
6、有序列表
前面的鏈表實現插入數據都是無序的,在有些應用中需要鏈表中的數據有序,這稱爲有序鏈表。
在有序鏈表中,數據是按照關鍵值有序排列的。一般在大多數需要使用有序數組的場合也可以使用有序鏈表。有序鏈表優於有序數組的地方是插入的速度(因爲元素不需要移動),另外鏈表可以擴展到全部有效的使用內存,而數組只能侷限於一個固定的大小中。
有序列表算法題:
將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是通過拼接給定的兩個鏈表的所有節點組成的。
示例:
輸入:1->2->4, 1->3->4
輸出:1->1->2->3->4->4
https://leetcode-cn.com/problems/merge-two-sorted-lists/
想法:遞歸
我們可以如下遞歸地定義在兩個鏈表裏的 merge 操作(忽略邊界情況,比如空鏈表等):
也就是說,兩個鏈表頭部較小的一個與剩下元素的 merge
操作結果合併。
算法
我們直接將以上遞歸過程建模,首先考慮邊界情況。
特殊的,如果 l1 或者 l2 一開始就是 null ,那麼沒有任何操作需要合併,所以我們只需要返回非空鏈表。否則,我們要判斷 l1 和 l2 哪一個的頭元素更小,然後遞歸地決定下一個添加到結果裏的值。如果兩個鏈表都是空的,那麼過程終止,所以遞歸過程最終一定會終止。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
else if (l2 == null) {
return l1;
}
else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}
else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
複雜度分析
- 時間複雜度:O(n + m) 因爲每次調用遞歸都會去掉 l1 或者 l2 的頭元素(直到至少有一個鏈表爲空),函數 mergeTwoList 中只會遍歷每個元素一次。所以,時間複雜度與合併後的鏈表長度爲線性關係。
-
空間複雜度:O(n + m)。調用 mergeTwoLists 退出時 l1 和 l2 中每個元素都一定已經被遍歷過了,所以 n + m個棧幀會消耗 O(n + m) 的空間。