文章目錄
前面的所謂動態數組,實際上是靠這種操作實現的,是對用戶來說的。而鏈表,是真正的動態數據結構,也是最簡單的動態數據結構(更難的有二叉搜索樹,trie樹,紅黑樹等)。
學習鏈表,可以讓我們更加深入的理解Java的引用/C++的指針;而且鏈表是遞歸數據結構,鏈表的操作基本上都可以用遞歸實現,我們能更深入的理解遞歸。
一、鏈表LinkedList
在鏈表中,數據存儲在結點中。一個結點的如果是,那它就是鏈表的尾結點。下面的實現中,將設計爲內部類。
class Node {
E e;
Node next;
}
鏈表的優點,在於真正的動態,不需要處理固定容量的問題,更高效的使用空間;缺點,喪失了隨機訪問的能力。
爲了提高鏈表的訪問能力,有設計了跳躍表這種數據結構,有時間我會在後面介紹。
二、無頭單鏈表實現
在LinkedList中,我們實現的是無頭的單鏈表,有一個Node
型的引用,它指向鏈表的第一個數據結點。
1. 基本的方法
Node(E e, Node next)
:的構造方法;Node(E e)
、Node()
:的構造方法;LinkedList()
:鏈表的構造方法size
:私有變量,表示鏈表中的元素個數,即鏈表的長度;head
:頭引用;getSize()
:得到鏈表的大小/長度;isEmpty()
:鏈表是否爲空。
2. 添加元素
對於單鏈表,在鏈表頭添加結點是最簡單的;而在數組中,在數組尾添加元素則是最簡單的。
-
addFirst(E e)
:在鏈表頭添加元素,; -
add(int index)
:在鏈表中間,索引爲的地方添加元素(),關鍵在於找到要添加的結點的前一個結點,其中如果在索引爲0的位置添加元素,沒有前一個結點,只有頭指針(引用),需要特判,;當然,真正使用鏈表的時候,我們很少使用這樣的操作,因爲選擇使用鏈表的時候,我們往往就選擇了不使用索引。後續我們使用鏈表組建其他的數據結構時,會完全摒棄掉這個操作;
-
addLast()
:複用add
方法,在的位置添加元素就好了,;
目前的無頭單鏈表的代碼如下:
public class LinkedList<E> {
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return e.toString();
}
}
private Node head;
private int size;
public LinkedList() {
head = null;
size = 0;
}
//獲取鏈表中的元素長度
public int getSize() {
return size;
}
//返回鏈表是否爲空
public boolean isEmpty() {
return size == 0;
}
//在鏈表頭添加元素
public void addFirst(E e) {
//Node node = new Node(e);
//node.next = head;
//head = node;
head = new Node(e, head);
++size;
}
//在鏈表的index(0-based)位置添加新的元素e
//不是常用的操作, 僅僅作爲練習
public void add(int index, E e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");
if (index == 0) //在無頭鏈表
addFirst(e);
else {
Node prev = head;
for (int i = 0; i < index - 1; ++i)
prev = prev.next;
//Node node = new Node(e);
//node.next = prev.next;
//prev.next = node;
prev.next = new Node(e, prev.next);
}
++size;
}
public void addLast(E e) {
add(size, e);
}
public static void main(String[] args) {
}
}
三、帶頭單鏈表的實現
1. 帶頭單鏈表添加元素的實現
我們發現前面的無頭單鏈表添加結點時,需要特判,不夠優雅…爲了統一操作,我們爲鏈表設立一個虛擬頭結點:。對於一個空鏈表,實際上是存在一個結點的,就是。這樣,即使在鏈表頭添加結點,也可以找到前一個結點,就是。
操作就是前面的add(int index, E e)
,addFirst(E e)
,addLast(E e)
。從出發,走步,走到要添加的位置的前一個結點處。
public class LinkedList<E> {
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return e.toString();
}
}
private Node dummyHead;
private int size;
public LinkedList() {
dummyHead = new Node(null, null); //虛擬頭結點初始化
size = 0;
}
//獲取鏈表中的元素長度
public int getSize() {
return size;
}
//返回鏈表是否爲空
public boolean isEmpty() {
return size == 0;
}
//在鏈表的index(0-based)位置添加新的元素e
//不是常用的操作, 僅僅作爲練習
public void add(int index, E e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");
Node prev = dummyHead; //從dummyhead開始遍歷
for (int i = 0; i < index; ++i)
prev = prev.next;
//Node node = new Node(e);
//node.next = prev.next;
//prev.next = node;
prev.next = new Node(e, prev.next);
++size;
}
//在鏈表頭添加元素
public void addFirst(E e) {
add(0, e);
}
public void addLast(E e) {
add(size, e);
}
public static void main(String[] args) {
}
}
2. 帶頭單鏈表的遍歷、查詢、修改
get(int index)
:獲得鏈表的位置(0-based)的元素,;getFirst()
:獲得鏈表的第一個元素,;getLast()
:獲得鏈表的最後一個元素,;set(int index, E e)
:修改鏈表的index(0-based)位置的元素爲e,;contains(E e)
:是否含有元素e,;
public class LinkedList<E> {
//.................
//獲得鏈表的index位置(0-based)的元素
public E get(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Get fail. Illegal index.");
Node cur = dummyHead.next;
for (int i = 0; i < index; ++i)
cur = cur.next;
return cur.e;
}
//獲得鏈表的第一個元素
public E getFirst() {
return get(0);
}
//獲得鏈表的最後一個元素
public E getLast() {
return get(size - 1);
}
//修改鏈表的index(0-based)位置的元素爲e
public void set(int index, E e) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Set fail. Illegal index.");
Node cur = dummyHead.next;
for (int i = 0; i < index; ++i)
cur = cur.next;
cur.e = e;
}
public boolean contains(E e) {
Node cur = dummyHead.next;
while (cur != null) {
if (cur.equals(e))
return true;
cur = cur.next;
}
return false;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
Node cur = dummyHead.next;
while (cur != null) {
res.append(cur.e + "->");
cur = cur.next;
}
res.append("NULL");
return res.toString();
}
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 5; ++i) {
linkedList.addFirst(i);
System.out.println(linkedList);
}
linkedList.add(2, 666);
System.out.println(linkedList);
}
}
3. 帶頭單鏈表元素的刪除【完全版本的代碼】
從出發,走步,走到要刪除元素的位置的前一個結點處,得到,讓,跳過,然後將,方便Java的垃圾回收。
remove(int index, E e)
:從鏈表中刪除index(0-based)位置的元素, 返回刪除的元素,;removeFirst()
:從鏈表中刪除第一個元素, 返回刪除的元素,;removeLast()
:從鏈表中刪除最後一個元素, 返回刪除的元素,。
public class LinkedList<E> {
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
public Node() {
this(null, null);
}
@Override
public String toString() {
return e.toString();
}
}
private Node dummyHead;
private int size;
public LinkedList() {
dummyHead = new Node(null, null); //虛擬頭結點初始化
size = 0;
}
//獲取鏈表中的元素長度
public int getSize() {
return size;
}
//返回鏈表是否爲空
public boolean isEmpty() {
return size == 0;
}
//在鏈表的index(0-based)位置添加新的元素e
//不是常用的操作, 僅僅作爲練習
public void add(int index, E e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");
Node prev = dummyHead; //從dummyhead開始遍歷
for (int i = 0; i < index; ++i)
prev = prev.next;
//Node node = new Node(e);
//node.next = prev.next;
//prev.next = node;
prev.next = new Node(e, prev.next);
++size;
}
//在鏈表頭添加元素
public void addFirst(E e) {
add(0, e);
}
public void addLast(E e) {
add(size, e);
}
//獲得鏈表的index位置(0-based)的元素
public E get(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Get fail. Illegal index.");
Node cur = dummyHead.next;
for (int i = 0; i < index; ++i)
cur = cur.next;
return cur.e;
}
//獲得鏈表的第一個元素
public E getFirst() {
return get(0);
}
//獲得鏈表的最後一個元素
public E getLast() {
return get(size - 1);
}
//修改鏈表的index(0-based)位置的元素爲e
public void set(int index, E e) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Set fail. Illegal index.");
Node cur = dummyHead.next;
for (int i = 0; i < index; ++i)
cur = cur.next;
cur.e = e;
}
public boolean contains(E e) {
Node cur = dummyHead.next;
while (cur != null) {
if (cur.equals(e))
return true;
cur = cur.next;
}
return false;
}
//從鏈表中刪除index(0-based)位置的元素, 返回刪除的元素
public E remove(int index) {
if (index < 0 || index >= size)
throw new IllegalArgumentException("Set fail. Illegal index.");
Node prev = dummyHead;
for (int i = 0; i < index; ++i)
prev = prev.next;
Node retNode = prev.next;
prev.next = retNode.next;
retNode.next = null;
--size;
return retNode.e;
}
//從鏈表中刪除第一個元素, 返回刪除的元素
public E removeFirst() {
return remove(0);
}
//從鏈表中刪除最後一個元素, 返回刪除的元素
public E removeLast() {
return remove(size - 1);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
Node cur = dummyHead.next;
while (cur != null) {
res.append(cur.e + "->");
cur = cur.next;
}
res.append("NULL");
return res.toString();
}
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < 5; ++i) {
linkedList.addFirst(i);
System.out.println(linkedList);
}
linkedList.add(2, 666);
System.out.println(linkedList);
linkedList.remove(2);
System.out.println(linkedList);
linkedList.removeFirst();
System.out.println(linkedList);
linkedList.removeLast();
System.out.println(linkedList);
}
}
四、簡單鏈表的總結
鏈表的增刪改查幾乎都是的,總體來說,性能弱於數組;但是我們發現,如果只對鏈表頭進行操作,如查、刪、添加,就是的時間。
通過其他的一些技巧,如同時保存指向頭結點和尾結點的指針,循環鏈表等,可以提高對鏈表尾的操作效率到。但是,對鏈表中間進行操作,幾乎都是線性時間。
這也提示我們,使用鏈表最好是對鏈表頭尾進行操作,滿足這樣的,就是棧和隊列。因此,後面將會實現鏈棧和鏈隊。
最後,鏈表是一個天然的遞歸數據結構,可以通過它很好的學習遞歸操作,下面會寫一篇文章對此進行總結。