【數據結構Java實現】單鏈表【最基本的動態數據結構】

前面的所謂動態數組,實際上是靠resizeresize這種操作實現的,是對用戶來說的。而鏈表,是真正的動態數據結構,也是最簡單的動態數據結構(更難的有二叉搜索樹,trie樹,紅黑樹等)。

學習鏈表,可以讓我們更加深入的理解Java的引用/C++的指針;而且鏈表是遞歸數據結構,鏈表的操作基本上都可以用遞歸實現,我們能更深入的理解遞歸

一、鏈表LinkedList

在鏈表中,數據存儲在結點NodeNode中。一個結點的nextnext如果是nullnull,那它就是鏈表的尾結點。下面的實現中,將NodeNode設計爲內部類。

class Node {
	E e;
	Node next;
}

鏈表的優點,在於真正的動態,不需要處理固定容量的問題,更高效的使用空間;缺點,喪失了隨機訪問的能力。

爲了提高鏈表的訪問能力,有dalaodalao設計了skiplistskiplist跳躍表這種數據結構,有時間我會在後面介紹。

二、無頭單鏈表實現

在LinkedList中,我們實現的是無頭的單鏈表,有一個Node型的引用headhead,它指向鏈表的第一個數據結點。

1. 基本的方法

  • Node(E e, Node next)NodeNode的構造方法;
  • Node(E e)Node()NodeNode的構造方法;
  • LinkedList():鏈表的構造方法
  • size:私有變量,表示鏈表中的元素個數,即鏈表的長度;
  • head:頭引用;
  • getSize():得到鏈表的大小/長度;
  • isEmpty():鏈表是否爲空。

2. 添加元素

對於單鏈表,在鏈表頭添加結點是最簡單的;而在數組中,在數組尾添加元素則是最簡單的。

  • addFirst(E e):在鏈表頭添加元素,O(1)O(1)

  • add(int index):在鏈表中間,索引爲indexindex的地方添加元素(index0index \geq 0),關鍵在於找到要添加的結點的前一個結點,其中如果在索引爲0的位置添加元素,沒有前一個結點,只有頭指針(引用),需要特判O(n)O(n)

    當然,真正使用鏈表的時候,我們很少使用這樣的操作,因爲選擇使用鏈表的時候,我們往往就選擇了不使用索引。後續我們使用鏈表組建其他的數據結構時,會完全摒棄掉這個操作;

  • addLast():複用add方法,在sizesize的位置添加元素就好了,O(n)O(n)

目前的無頭單鏈表的代碼如下:

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. 帶頭單鏈表添加元素的實現

我們發現前面的無頭單鏈表添加結點時,需要特判,不夠優雅…爲了統一操作,我們爲鏈表設立一個虛擬頭結點:dummyHeaddummyHead。對於一個空鏈表,實際上是存在一個結點的,就是dummyHeaddummyHead。這樣,即使在鏈表頭添加結點,也可以找到前一個結點,就是dummyHeaddummyHead

操作就是前面的add(int index, E e)addFirst(E e)addLast(E e)。從dummyHeaddummyHead出發,走indexindex步,走到要添加的位置的前一個結點處。

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):獲得鏈表的indexindex位置(0-based)的元素,O(n)O(n)
  • getFirst():獲得鏈表的第一個元素,O(1)O(1)
  • getLast():獲得鏈表的最後一個元素,O(n)O(n)
  • set(int index, E e):修改鏈表的index(0-based)位置的元素爲e,O(n)O(n)
  • contains(E e):是否含有元素e,O(n)O(n)
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. 帶頭單鏈表元素的刪除【完全版本的代碼】

dummyHeaddummyHead出發,走indexindex步,走到要刪除元素的位置的前一個結點處,得到prevprev,讓prev.next=delNode.nextprev.next = delNode.next,跳過delNodedelNode,然後將delNode.next=nulldelNode.next = null,方便Java的垃圾回收。

  • remove(int index, E e):從鏈表中刪除index(0-based)位置的元素, 返回刪除的元素,O(n)O(n)
  • removeFirst():從鏈表中刪除第一個元素, 返回刪除的元素,O(1)O(1)
  • removeLast():從鏈表中刪除最後一個元素, 返回刪除的元素,O(n)O(n)
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);
	} 
}

在這裏插入圖片描述

四、簡單鏈表的總結

鏈表的增刪改查幾乎都是O(n)O(n)的,總體來說,性能弱於數組;但是我們發現,如果只對鏈表頭進行操作,如查、刪、添加,就是O(1)O(1)的時間。

通過其他的一些技巧,如同時保存指向頭結點和尾結點的指針,循環鏈表等,可以提高對鏈表尾的操作效率到O(1)O(1)。但是,對鏈表中間進行操作,幾乎都是線性時間。

這也提示我們,使用鏈表最好是對鏈表頭尾進行操作,滿足這樣的,就是棧和隊列。因此,後面將會實現鏈棧和鏈隊。

最後,鏈表是一個天然的遞歸數據結構,可以通過它很好的學習遞歸操作,下面會寫一篇文章對此進行總結。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章