數據結構梳理(2) - 線性表的鏈式表示之鏈表

前言

發一下牢騷,本來這個數據結構梳理的系列是在我找工作之前開始的,但是在中間找工作的過程中,一部分原因是面試太忙時間比較少,只能舍重就輕,當然更大的原因還是我自己的惰性,爲了保持這個系列的完整性,以及自己日後的複習,於是決定重新開始,按照之前的思路,將這個系列梳理完,中間也會穿插一些關於這些數據結構在面試中的考題。

好了,上一篇講的最基礎的線性表的順序表示,也就是基於數組,那麼本篇的主要內容就是線性表的鏈式表示,簡言之就是鏈表

目錄

1、鏈表的概念
2、鏈表的基本操作
3、鏈表的優缺點
4、鏈表和數組的對比
5、單(雙)向鏈表的完整代碼

正文

1、鏈表的概念

用一組任意的存儲單元存儲線性表的數據元素的的數據結構。

很簡單,值得一說的是,這個任意的存儲單元,既可以是連續的,也可以是不連續的,在這個概念中的“單元”這個詞,其實就是我們所熟知的節點,每個節點包含兩個域,一個數據域,一個指針域或者兩個指針域,如果是一個指針域,就是單向鏈表,如果是兩個指針域,就是雙向鏈表。

2、鏈表的基本操作

因爲這裏只是基本操作,所以就拿單向鏈表來舉例子,雙向鏈表只是比單向鏈表多了個指針域,只要把單向鏈表弄明白了,對應雙向鏈表的基本操作就自然而然會了。

a.初始化
上一節說線性表的順序表示時,也說過初始化,這裏的初始化也大同小異,主要包括這樣一些值的確定,一個是初始容量,另一個就是空節點的值。

空節點的值可以使用int類型的默認值,也就是0,因爲是鏈表,所以初始容量一般是0,還記得線性表初始化時的初始容量和負載因子這兩個概念嗎,這裏之所以不需要這兩個東東,是因爲我們在聲明一個數組的時候,必須要給它一個初始容量,而鏈表就比較靈活了,需要一個元素直接鏈上去就行。

由於是鏈表,另一個需要初始化確定的就是節點,因爲這裏以單向鏈表來說明,所以每個節點一個數據成員變量,一個next指針成員變量,由於內部類的特性,所以節點類一般聲明爲內部類,如下

	public class Node {

		private int data;
		private Node next;

		public Node() {

		}

		public int getData() {
			return data;
		}

		public Node(int data, Node next) {
			this.data = data;
			this.next = next;
		}

	}

然後是單向鏈表類的初始化,按照上面描述的,如下

	private Node head;//頭結點
	private Node tail;//尾節點
	private int size = 0;//鏈表長度

	public SingleLinkList() {
		head = null;
		tail = null;
		size = 0;

	}
	
	public SingleLinkList(int data) {
		head = new Node(data, null);
		tail = head;
		size++;
	}

爲了使用的方便,這裏額外加了個使用一個元素值初始化鏈表的構造方法,不過這個不重要,看使用的需求即可,也可爲了方便,自己加上兩個、三個等元素的構造方法來初始化。

b.增加元素

增加元素爲了增加的效率,這裏分爲三種情況,增加在鏈表頭的位置,增加在鏈表尾的位置,增加在鏈表中間的位置。所以我們可以寫三個對應的方法。其中相對複雜點的就是增加在鏈表中間的位置,對應的方法代碼如下

	public void addAtIndex(int data, int index) {
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (head == null) {
			addAtTail(data);
		} else {
			if (index == 0) {
				addAtHead(data);
			} else {
				Node preNode = findNodeByIndex(index - 1);//找到前一個節點
				preNode.next = new Node(data, preNode.next);
				size++;
			}
		}
	}

如果不是自己動手寫的話,還是會發現即便只是一個增加代碼,還是要考慮很多細節問題的,比如,一開始判斷插入位置的合法性,以及鏈表的判空,這樣就可以直接使用尾插來增加元素了,如果查找到應插在鏈表頭,則使用頭插來插入這個元素,否則,我們才手動進行元素的插入。因爲我使用了Node的構造方法,所以這裏我只用了一行代碼,其實也是最核心的代碼,大致分爲兩步,第一步,將待插入節點的next指針指向插入位置的next指針指向的節點,第二步,將前一個節點的next指針指向待插入節點。

然後再貼上頭插和尾插的方法代碼,頭插的代碼如下

	public void addAtHead(int data) {
		if (head == null) {
			head = new Node(data, null);
			tail = head;
		} else {
			Node newNode = new Node(data, head);
			head = newNode;
		}
		size++;
	}

尾插的代碼如下

	public void addAtTail(int data) {
		if (head == null) {
			head = new Node(data, null);
			tail = head;
		} else {
			Node newNode = new Node(data, null);
			tail.next = newNode;
			// 將尾指針指向最新的最後一個節點
			tail = newNode;
		}
		size++;
	}

c.刪除元素
刪除元素,爲了使用的方便性,也寫了兩個方法,一個是根據下標來刪除,也就是從鏈表頭開始的小標,另一種是根據元素值來刪除。

首先我們來看根據下標來刪除元素,代碼如下

	// 刪除指定位置的節點
	public void deleteByIndex(int index) {
		if (head == null) {
			System.out.println("鏈表爲空");
		} else {
			if (index < 0 || index >= size) {
				throw new RuntimeException("越界啦");
			}
			Node deleteNode = null;
			if (index == 0)// 刪除頭節點
			{
				deleteNode = head;
				head = head.next;
				size--;
			} else if (index == size - 1) {// 刪除尾節點
				deleteNode = tail;
				Node prevNode = findNodeByIndex(index - 1);
				prevNode.next = null;
				tail = prevNode;
				size--;
			} else {
				Node prevNode = findNodeByIndex(index - 1);// 獲取要刪除的節點的前一個節點
				deleteNode = prevNode.next;// 要刪除的節點就是prev的next指向的節點
				prevNode.next = deleteNode.next;// 刪除以後prev的next指向被刪除節點之前所指向的next
				size--;
			}
			deleteNode = null;
		}
	}

同樣的,核心操作也只有三步,代碼註釋寫的很清楚所以就不贅述了,但是爲了健壯性和效率性,我們要儘可能的考慮到所有的情況,來優化我們的代碼。

接下來是根據元素值來定向刪除,代碼如下

	// 刪除指定值的節點,如果存在這個節點,則刪除,否則不作處理
	public void deleteByData(int data) {
		int index = findIndexByData(data);
		if (index == -1) {
			System.out.println("鏈表爲空");
		} else if (index == -2) {
			System.out.println("未找到對應的元素");
		} else {
			deleteByIndex(index);
		}
	}

其核心就是調用了一個查找方法和上面的刪除方法,查找方法後面會說到,先不急,我們主要是思路。當然爲了效率, 我們也可以寫一個尾刪的方法,三行搞定

	// 刪除 鏈表中最後一個元素
	public void deleteLast() {
		deleteByIndex(size - 1);
	}

所以我們只要弄懂了最核心的方法,其它的我們都可以去利用這個方法自由擴展。

d.查找元素
好了,到了最後一個基本操作,就是查找元素。同樣的,根據我們平時使用的需求,主要是分爲兩種,根據下標查找對應的值,以及已知值查找對應的下標。

我們首先看第一種查找,根據下標查找對應的值,代碼如下

	// 通過index查找指定的節點
	public Node findNodeByIndex(int index) {
		if (head == null) {
			return null;
		}
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (index == 0) {
			return head;
		}
		if (index == size - 1) {
			return tail;
		}
		Node current = head;
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (i == index) {
				return current;
			}
		}
		return null;
	}

首先是一連串的判斷,然後來到一個核心的循環,由於是單向鏈表,所以只能從鏈表頭開始一個個去遍歷。弄明白了這個方法,那麼第二種查找,根據值來查找對應的下標也就是小case了。代碼如下

	// 查找指定元素的位置
	public int findIndexByData(int data) {
		if (head == null) {
			return -1;// 數組爲空
		}
		if (head.data == data) {
			return 0;
		}
		if (tail.data == data) {
			return size - 1;
		}
		Node current = head;// 從第一個節點開始查找對比數據
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (current.data == data)
				return i;
		}
		return -2;// 未找到對應的節點
	}

要多說一下的是,如果值沒有找到,我這裏是返回的負數,來代表不同的含義,當然在實際中,最好是拋出異常處理,這樣便於代碼的異常排查。

3、鏈表的優缺點

在熟悉了鏈表的基本操作之後,我們可以很明顯的發現一個問題,就是鏈表的插入和刪除非常簡單,但是鏈表的遍歷就有點麻煩了,總感覺沒有數組方便,數組直接定位,真快,但是鏈表還得從頭開始去遍歷,所以對於鏈表來說,優點是插入和刪除效率高,但是查找效率低,相應的,鏈表適用於大量插入刪除的場景,而對於一些需要頻繁查找的場景就不那麼適合了。

4、鏈表和數組的對比與合體

分析了一波鏈表的優缺點之後,我們再回想下數組,發現數組的優缺點和鏈表是正好反着的,數組是適用於查找,但是插入刪除效率低,因爲涉及到元素大量移動的問題,而鏈表正好彌補了數組的缺點,但是它卻在查找方面表現不佳。

所以沒有哪一種是完美的,或者說通用的,這二者的取捨需要我們根據實際的使用場景來自行選擇。

那麼我們是否想過這樣一個問題,既然這兩個互補,那我們能不能有一種東西把這兩個對象的優點綜合起來呢,這豈不是很完美了,對吧,當然,Java的設計者當然考慮過這個問題,於是他們設計了一個容器叫做HashMap,嘿嘿嘿,想了解它的原理嗎?想了解其中的奧祕嗎?想知道優秀的Java設計者是如何綜合數組和鏈表的優勢的嗎?限於篇幅,我就不詳解了,這裏我就丟下三個字:哈希表。剩下的就是你們自己去研究啦,hhhh…

5、單(雙)向鏈表的完整代碼

單向鏈表的完整代碼如下

public class SingleLinkList {

	public class Node {

		private int data;
		private Node next;

		public Node() {

		}

		public int getData() {
			return data;
		}

		public Node(int data, Node next) {
			this.data = data;
			this.next = next;
		}

	}

	private Node head;
	private Node tail;
	private int size = 0;

	public SingleLinkList() {
		head = null;
		tail = null;
		size = 0;

	}

	public Node getHead() {
		return head;
	}

	public Node getTail() {
		return tail;
	}

	public SingleLinkList(int data) {
		head = new Node(data, null);
		tail = head;
		size++;
	}

	public int getLength() {
		return size;
	}

	public void addAtHead(int data) {
		if (head == null) {
			head = new Node(data, null);
			tail = head;
		} else {
			Node newNode = new Node(data, head);
			head = newNode;
		}
		size++;
	}

	public void addAtTail(int data) {
		if (head == null) {
			head = new Node(data, null);
			tail = head;
		} else {
			Node newNode = new Node(data, null);
			tail.next = newNode;
			// 將尾指針指向最新的最後一個節點
			tail = newNode;
		}
		size++;
	}

	public void addAtIndex(int data, int index) {
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (head == null) {
			addAtTail(data);
		} else {
			if (index == 0) {
				addAtHead(data);
			} else {
				Node preNode = findNodeByIndex(index - 1);
				preNode.next = new Node(data, preNode.next);
				size++;
			}
		}
	}

	// 通過index查找指定的節點
	public Node findNodeByIndex(int index) {
		if (head == null) {
			return null;
		}
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (index == 0) {
			return head;
		}
		if (index == size - 1) {
			return tail;
		}
		Node current = head;
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (i == index) {
				return current;
			}
		}
		return null;
	}

	// 查找指定元素的位置
	public int findIndexByData(int data) {
		if (head == null) {
			return -1;// 數組爲空
		}
		if (head.data == data) {
			return 0;
		}
		if (tail.data == data) {
			return size - 1;
		}
		Node current = head;// 從第一個節點開始查找對比數據
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (current.data == data)
				return i;
		}
		return -2;// 未找到對應的節點
	}

	// 刪除指定位置的節點
	public void deleteByIndex(int index) {
		if (head == null) {
			System.out.println("鏈表爲空");
		} else {
			if (index < 0 || index >= size) {
				throw new RuntimeException("越界啦");
			}
			Node deleteNode = null;
			if (index == 0)// 刪除頭節點
			{
				deleteNode = head;
				head = head.next;
				size--;
			} else if (index == size - 1) {// 刪除尾節點
				deleteNode = tail;
				Node prevNode = findNodeByIndex(index - 1);
				prevNode.next = null;
				tail = prevNode;
				size--;
			} else {
				Node prevNode = findNodeByIndex(index - 1);// 獲取要刪除的節點的前一個節點
				deleteNode = prevNode.next;// 要刪除的節點就是prev的next指向的節點
				prevNode.next = deleteNode.next;// 刪除以後prev的next指向被刪除節點之前所指向的next
				size--;
			}
			deleteNode = null;
		}
	}

	// 刪除指定值的節點,如果存在這個節點,則刪除,否則不作處理
	public void deleteByData(int data) {
		int index = findIndexByData(data);
		if (index == -1) {
			System.out.println("鏈表爲空");
		} else if (index == -2) {
			System.out.println("未找到對應的元素");
		} else {
			deleteByIndex(index);
		}
	}

	// 刪除 鏈表中最後一個元素
	public void deleteLast() {
		deleteByIndex(size - 1);
	}

	// 更新指定位置的值
	public void updateByIndex(int data, int index) {
		findNodeByIndex(index).data = data;
	}

	// 清除鏈表中所有的元素
	public void clear() {
		head = null;
		tail = null;
		size = 0;
	}

	// 判斷鏈表是否爲空
	public boolean isEmpty() {
		return size == 0;
	}

	// 鏈表的輸出 重寫toString方法
	public String toString() {
		if (isEmpty()) {
			return "null";
		} else {
			StringBuilder sb = new StringBuilder("");
			for (Node current = head; current != null; current = current.next)// 從head開始遍歷
			{
				sb.append(current.data + "-");
			}
			int len = sb.length();
			return sb.delete(len - 1, len).append("").toString();// 刪除最後一個 -
		}
	}
	
	public static void main(String[] args) {		
		SingleLinkList list = new SingleLinkList();

		list.addAtHead(1);
		list.addAtHead(2);
		list.addAtHead(3);
		list.addAtTail(4);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.addAtIndex(9, 3);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.deleteByIndex(4);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.deleteByData(2);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.deleteLast();
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());
		
		list.addAtTail(5);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.addAtTail(5);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());

		list.updateByIndex(66, 3);
		System.out.println(list+"  size="+list.getLength()+"   head="+list.getHead().getData()+"   tail="+list.getTail().getData());
		
		list.clear();
		System.out.println(list+"  size="+list.getLength());

	}
}

同理,我們可以稍加擴展,寫出雙向鏈表的代碼,如下

public class DoubleLinkList {

	public class Node {
		private int data;
		private Node pre;
		private Node next;

		public Node() {

		}

		public int getData() {
			return data;
		}
		
		public Node getPre() {
			return pre;
		}

		public Node(int data, Node pre, Node next) {
			this.data = data;
			this.pre = pre;
			this.next = next;
		}
	}

	private Node head;
	private Node tail;
	private int size = 0;

	public DoubleLinkList() {
		head = null;
		tail = null;
		size = 0;
	}

	public Node getHead() {
		return head;
	}

	public Node getTail() {
		return tail;
	}

	public DoubleLinkList(int data) {
		head = new Node(data, null, null);
		tail = head;
		size++;
	}

	public int getLength() {
		return size;
	}

	public void addAtHead(int data) {
		if (head == null) {
			head = new Node(data, null, null);
			tail = head;
		} else {
			Node newNode = new Node(data, null, head);
			head = newNode;
		}
		size++;
	}

	public void addAtTail(int data) {
		if (head == null) {
			head = new Node(data, null, null);
			tail = head;
		} else {
			Node newNode = new Node(data, tail, null);
			tail.next = newNode;
			// 將尾指針指向最新的最後一個節點
			tail = newNode;
		}
		size++;
	}

	public void addAtIndex(int data, int index) {
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (head == null) {
			addAtTail(data);
		} else {
			if (index == 0) {
				addAtHead(data);
			} else {
				Node preNode = findNodeByIndex(index - 1);
				Node addNode = new Node(data, preNode, preNode.next);
				preNode.next.pre = addNode;// 先設置後一個節點的前驅
				preNode.next = addNode;// 再設置前一個節點的後繼
				size++;
			}
		}
	}

	// 通過index查找指定的節點
	public Node findNodeByIndex(int index) {
		if (head == null) {
			return null;
		}
		if (index < 0 || index >= size) {
			throw new RuntimeException("越界啦");
		}
		if (index == 0) {
			return head;
		}
		if (index == size - 1) {
			return tail;
		}
		Node current = head;
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (i == index) {
				return current;
			}
		}
		return null;
	}

	// 查找指定元素的位置
	public int findIndexByData(int data) {
		if (head == null) {
			return -1;// 數組爲空
		}
		if (head.data == data) {
			return 0;
		}
		if (tail.data == data) {
			return size - 1;
		}
		Node current = head;// 從第一個節點開始查找對比數據
		for (int i = 0; i < size & current.next != null; i++, current = current.next) {
			if (current.data == data)
				return i;
		}
		return -2;// 未找到對應的節點
	}

	// 刪除指定位置的節點
	public void deleteByIndex(int index) {
		if (isEmpty()) {
			throw new RuntimeException("鏈表爲空");
		} else {
			if (index < 0 || index >= size) {
				throw new RuntimeException("越界啦");
			}
			Node deleteNode = null;
			if (index == 0)// 刪除頭節點
			{
				Node current = head.next;
				current.pre = null;
				deleteNode = head;
				head = current;
				size--;
			} else if (index == size - 1) {// 刪除尾節點
				deleteNode = tail;
				Node prevNode = findNodeByIndex(index - 1);
				prevNode.next = null;
				tail = prevNode;
				size--;
			} else {
				Node prevNode = findNodeByIndex(index - 1);// 獲取要刪除的節點的前一個節點
				deleteNode = prevNode.next;// 要刪除的節點就是prev的next指向的節點
				prevNode.next = deleteNode.next;// 刪除以後prev的next指向被刪除節點之前所指向的next
				deleteNode.next.pre = prevNode;// 設置刪除節點的後繼節點的前驅等於 刪除節點的前驅
				size--;
			}
			deleteNode = null;
		}
	}

	// 刪除指定值的節點,如果存在這個節點,則刪除,否則不作處理
	public void deleteByData(int data) {
		int index = findIndexByData(data);
		if (isEmpty()) {
			throw new RuntimeException("鏈表爲空");
		} else if (index == -2) {
			System.out.println("未找到對應的元素");
		} else {
			deleteByIndex(index);
		}
	}

	// 刪除 鏈表中最後一個元素
	public void deleteLast() {
		deleteByIndex(size - 1);
	}

	// 更新指定位置的值
	public void updateByIndex(int data, int index) {
		findNodeByIndex(index).data = data;
	}

	// 判斷鏈表是否爲空
	public boolean isEmpty() {
		return size == 0;
	}

	// 清除鏈表中所有的元素
	public void clear() {
		head = null;
		tail = null;
		size = 0;
	}

	// 鏈表的輸出 重寫toString方法
	public String toString() {
		if (isEmpty()) {
			return "null";
		} else {
			StringBuilder sb = new StringBuilder("");
			for (Node current = head; current != null; current = current.next)// 從head開始遍歷
			{
				sb.append(current.data + "-");
			}
			int len = sb.length();
			return sb.delete(len - 1, len).append("").toString();// 刪除最後一個 -
		}
	}
	
	public static void main(String[] args) {
		DoubleLinkList list = new DoubleLinkList();
		list.addAtHead(1);
		list.addAtHead(2);
		list.addAtHead(3);
		list.addAtTail(4);
		list.addAtTail(5);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.addAtIndex(9, 0);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.deleteByIndex(2);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.deleteByData(4);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.deleteLast();
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.updateByIndex(55, 1);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.updateByIndex(44, 0);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());
		list.updateByIndex(66, 2);
		System.out.println(list + "  size=" + list.getLength() + "   head=" + list.getHead().getData() + "   tail="+ list.getTail().getData());


	}
}

結語

好了,本篇就到此爲止了,雖然內容有點多,但是原理都比較簡單,主要是要自己動手,基本上手動實現一遍就差不多了,按照計劃,下一篇應該是棧的梳理,下一篇見!!!

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