數據結構——鏈式存儲結構(單向鏈表)

關於順序存儲結構的內容,就止步於雙端隊列了。接下來要介紹的是鏈表的內容,相對用於實現順序表的數組來說,鏈表的定義即實現就比較抽象和麻煩一些,而且還難!but!雖然難,“我”也不會氣餒!該說還得說啊,雖然自己是個菜鳥,但是總有一天,......算了,先介紹一下鏈表是啥吧。

先來看一條生活中常見的鏈子:

我們發現,生活中不管金鍊子也好、還是銀鏈子也好、亦或者是鐵的都是由一個小小的連在一起,然後組成了一條鏈子。也就是說:

                                                        

所以運用生活中的原理,我們就創建出了鏈表這一概念。

動態鏈表

爲了表示每個數據元素ai與其直接後繼數據元素ai+1 之間的邏輯關係,對數據元素ai來說,除了存儲其本身的信息之外,還需存儲一個指示其直接後繼的信息(即直接後繼的存儲位置)。我們把存儲數據元素信息的域稱爲數據域,把存儲直接後繼位置的域稱爲指針域。指針域中存儲的信息稱做指針或鏈。這兩部分信息組成數據元素ai的存儲映像,稱爲結點(Node)。【這是官方定義!】

結點

簡單點來說吧!這個結點就可以看成上面圖片中的一個環,然後然後環環相連,組成一條的鏈子就是鏈表,與實際的鏈子不同的是,實際的鏈子中的環是空的,但是它套着下一個環,而在數據結構中,這個結點賦予了特殊含義,首先它是一個類,然後這個類裏面有兩個屬性:一個是數據域,存儲我們需要存儲的數據;另一個是指針域,存儲的是下一個結點所在的地址,方便我們找到該結點的位置。就是因爲有這個指針域將下一個結點的地址存儲,才形成了邏輯上的鏈式存儲。結點以及鏈表大概長這個樣子:

                  

因此,n個結點(ai 的存儲映像)鏈結成一個鏈表,即爲線性表(a1, a2, ....,an)的鏈式存儲結構,因爲此鏈表的每個結點中只包含一個指針域, 所以叫做單鏈表

頭指針和尾指針

那麼問題來了。我們知道每個結點的指針域存儲的是下一個結點所在的位置,這個位置是內存隨機分配的,我們並不知道。因此,第一個結點的位置我們是不知道的,那麼這個鏈表該從哪兒開始查找呢?爲了解決這個問題,需要設置一個頭指針。

我們把鏈表當中的第一個結點叫做頭結點,在鏈式存儲的結構當中,頭結點可虛可實,什麼意思呢?就是分爲真實的頭結點——它存儲元素,也存儲下一個結點的地址,是真真正正的老大!虛擬的頭結點——不存儲元素,只存儲下一個結點的地址,他不是真真的老大,真正的老大是它的下一個,但是虛擬頭結點是一個真真切切的結點!表現形式如下:

 

因此,爲了拿住這個鏈表,擒賊先擒王,首先要獲得這個鏈表的頭結點的位置,所以,需要一個指針並且我們可以訪問的到,來存儲頭結點的地址——這個就是頭指針,而這個指針僅僅是一個引用變量而已,只用來存儲頭結點的地址

當然做事要有始有終,既然有頭指針,相應的就應該有尾指針,表示該鏈表最後一個結點的位置存儲的是最後一個結點的地址,以便遍歷的時候,判斷是否遍歷到最後一個位置。當然我們在鏈表結構中也會設置一個size表示長度,然而現在是鏈式存儲的結構,我們應該用鏈式的特性去操作鏈表,不推薦用size。當然設置尾指針還有一個好處就是,因爲要查找到某個結點時,必須要查找到其前一個結點,每次都要從頭遍歷,如果要實現尾插法的話,每次尾插就要遍歷一遍,此時有尾指針就可直接獲得最後一個元素的地址,把當前尾結點的指針域指向新進來結點的地址,然後尾指針後移就可。省去了遍歷的時間複雜度

線性表

至此有關動態鏈表的內容就介紹完了,然後就是具體的實現。在順序存儲結構中有線性表,是用數組的實現的,現在已經知道了鏈表的內容,這裏我們可以用鏈表實現一個線性表。具體方法如類圖,具體方法的實現如代碼:

鏈表實現的線性表仍然是繼承List接口,其實在Java內置工具中,List接口下面也有一個LinkedList子類,就是拿鏈表實現的。鏈表是由結點組成的,因此LinkedList類裏面包含一個Node類,在之前介紹鏈棧和內部內的時候說過這個結構。兩個類之前的關係應該是組合關係,因爲鏈表沒了結點就不能是鏈表了。但又是部分與整體的關係,所有這裏是組合或者聚合關係都對。

然後我們再來看看用鏈表怎麼實現線性表的函數操作(本次實現的線性表採用的是有虛擬頭結點的結構實現):

package 線性表; //目前我們所寫的所有代碼包括實現的接口都是我們自己寫的

public class LinkedList<E> implements List<E>{

	private class Node{  //首先定義一個結點類,這個類是是組成鏈表的關鍵,
    //相當於也是鏈表的一個屬性,因此要定義成一個私有的,外界就不能創建結點改變鏈表內部的結構了
		E data;  //數據域,因爲在結點的外部,我們在給結點的數據域存放數據之後,還要能訪問到,因此是共有的
		Node next; //指針域,同數據域
		public Node() { //創建Node不帶參的構造函數
			this(null,null);
		}
		public Node(E data,Node next) {  //創建Node代參的構造函數
			this.data=data;
			this.next=null;
		}

		@Override
		public String toString() {  //重寫Node的toString方法,方便在打印出結果時,
                                        //是我們想要看到的數據,而不是地址或其他
			return data.toString();
		}
	}
	
	private Node head; //指向頭結點的頭指針 
	private Node rear;  //指向尾結點的尾指針
	private int size;  //記錄元素個數
	
	public LinkedList() { //LinkedList的構造函數,初始狀態是有一個虛擬的頭結點,
                                //頭尾指針都指向這個虛擬的頭結點,有效元素爲0個
		head = new Node();
		rear =head;
		size=0;
	}
	
	public LinkedList(E[] arr) {//帶參的構造函數,傳進來一個數組,然後將數組裏面的內容轉化成鏈表存儲
		this(); //先初始化一下,創建一個虛擬頭結點,初始化頭尾指針和有效元素的個數
		for(E e:arr) {  //從頭到尾遍歷數組
			addLast(e); //將遍歷的內容依次插入到鏈表的尾部
		}
	}
	
	@Override
	public int getSize() {  //獲取鏈表的長度
		return size;  //返回有效元素個數即可虛擬頭結點不算
	}

	@Override
	public boolean isEmputy() {//判空,查看是否是在初始化的狀態,切記虛擬頭結點不算一個元素
		return size==0&&head.next==null;
	}

   然後我們看看鏈表怎麼進行插入:

在頭插:

在尾插:

在中間插:

 

因此代碼實現一個指定角標插入元素:

@Override
	public void add(int index, E e) {
		if(index<0 || index>size) {  //先判斷添加的位置是否合法
			throw new IllegalArgumentException("插入角標非法!");
		}
		Node n = new Node(); //如果合法,肯定會有一個新的結點創建
		n.data=e; //新結點的數據域存儲的是要插入的元素
		if(index==0) { //如果插入的位置在頭結點
			n.next=head.next; //先將真實頭結點的地址存儲在新結點的指針域裏
			head.next=n;  //然後改朝換代,將虛擬頭結點的指針域的內容換成添加的新節點的地址
			if(size==0) { //此時判斷是否是第一次添加,第一次添加有效元素肯定爲零,
//那麼頭尾指針都指向的是虛擬頭結點的位置,現在新添加了一個結點,那麼尾指針肯定指向新添加結點的位置
//如果再來一個新結點,往頭部插入,那麼尾指針就不用移動了,可參照圖解去理解
				rear=n;
			}
		}else if(index ==size) { //如果在尾部添加
			rear.next=n; //直接將新添加的結點的地址存放在當前尾結點的指針域裏
			rear=rear.next; //然後尾指針存放的地址是新的結點的地址
		}else { //否則的話,在中間插入
			Node p =head; //需要從頭遍歷
			for(int i=0;i<index;i++) {
				p=p.next; //遍歷到要刪除結點的前一個結點
			}
			n.next=p.next; //把新結點的指針域存放要取代位置的結點的地址
			p.next=n; //然後把新結點的地址放到取代位置的前一個結點的指針域裏
		}
		size++; //只要添加了,最後的有效元素都增加1喔
	}

	@Override
	public void addFirst(E e) { //頭插
		add(0,e); //調用方法,傳0
	}

	@Override
	public void addLast(E e) { //尾插
		add(size,e); //調用方法傳尾
	}

	@Override
	public E get(int index) { //獲取某個位置結點的內容
		if(index<0 || index>size) { //首先判斷取的位置是否在鏈表範圍內
			throw new IllegalArgumentException("查找角標非法!");
		}
		if(index==0) { //如果取頭
			return head.next.data; //直接返回頭指針指向的下一個結點的數據
		}else if(index==size-1){ //如果取尾
			return rear.data;  //直接返回尾指針指向的結點的數據
		}else {  //如果取中間位置
			Node p = head;  //從頭遍歷
			for(int i=0;i<=index;i++) {
				p=p.next; //找到要取元素的結點
			}
			return p.data; //返回該結點的數據
		}
	}

	@Override
	public E getFirst() { //取頭
		return get(0); //傳入0
	}

	@Override
	public E getLast() { //取尾
		return get(size-1);  //傳入尾
	}

	@Override
	public void set(int index, E e) {  //修改某一位置的數據
		if(index<0 || index>size) { //首先還是要判斷修改的位置是否在鏈表範圍內
			throw new IllegalArgumentException("修改角標非法!");
		}
		if(index==0) { //修改的是頭
			head.next.data=e; //直接將新元素取代頭結點的數據
		}else if(index==size-1){ //修改的是尾
		    rear.data=e;  //將尾指針指向的尾結點的數據更新
		}else { //如果修改的是中間位置
			Node p = head;  //還是要從頭遍歷
			for(int i=0;i<=index;i++) {
				p=p.next; //遍歷到該結點
			}
		    p.data=e; //直接給數據域賦值
		}
	}

	@Override
	public boolean contains(E e) { //判斷是否包含某個元素
		return find(e)!=-1; //首先要根據元素查找是否存在該元素存儲在結點的位置,如果爲-1表示不存在,不爲-1,表示存在
	}

	@Override
	public int find(E e) { //查找某元素的下標
		int index=-1;//首先表示不存在
		if(isEmputy()) { //如果鏈表爲空
			return -1; //肯定不存在
		}else { //不爲空,就要遍歷查找
			Node p=head; //從虛擬頭結點開始
			while(p.next!=null) { //只要下一個結點不爲空
				p=p.next;將真正的頭取代p
				index++;表示的位置在增加
				if(p.data==e) { //如果此結點存儲的數據與我們要找的數據相等
					return index; //將此結點的位置返回
				}//否則繼續向下找
			}
		}
		return -1; //當循環正常結束也是沒中斷返回,那就是不存在,返回-1
	}

如何刪除一個結點:

刪頭:

刪尾:

 

刪除中間結點:

 

代碼實現:

@Override
	public E remove(int index) { //指定角標刪除結點
		if(index<0 || index>size) {  //判斷指定角標是否在合法範圍內
			throw new IllegalArgumentException("刪除角標非法!");
		}
		if(index==0) { //刪頭
			Node p=head.next; //先將頭結點提出來
			E e = p.data; //將頭結點的數據取出來
			head.next=p.next; //將頭結點的下個結點的地址存到虛擬頭結點的指針域裏
			p.next=null; //將原頭結點指針域置空與原鏈表斷開關係
			p.data=null; //數據域清空
			if(size==1) { //此時特殊在刪除的是最後一個結點
				rear=head; //那麼尾指針要指向虛擬頭結點的位置
			}
			size--; //刪除後有效元素減少1
			return e; //將已經刪除的元素返回
		}else if(index==size-1) { //刪尾
			Node p=head; //從頭遍歷
			E e = rear.data; //將尾指針的數據取出來
			while(p.next!=rear) { //遍歷到尾結點的前一個結點
				p=p.next;
			}
			p.next=null; //將尾結點前一個結點中存的尾結點的地址清空
			rear=p; //此時尾指針指向尾結點的前一個結點的位置
			size--; 
			return e;
		}else { //刪中間
			Node p=head; //從頭遍歷
			for(int i=0;i<index;i++) {
				p=p.next;
			} //遍歷到要刪位置的前一個結點
			Node d = p.next; //將要刪除的結點取出來
			E e= d.data; //再將要刪除結點的數據取出來
			p.next=d.next; //將要刪除結點的下一個結點的地址存到要刪結點的前一個結點的指針域
			d.next=null; //此結點指針域置空
			d.data=null; //數據域置空
			size--;
			return e;
		}
	}

	@Override
	public E removeFirst() { //只刪頭
		return remove(0);
	}

	@Override
	public E removeLast() { //只刪尾
		return remove(size-1);
	}

	@Override
	public void removeElement(E e) { //刪除指定的數據元素
       int index =find(e); //先找到要刪元素的位置
       if(index==-1) { //元素不存在,報錯
			throw new IllegalArgumentException("元素不存在!");
		}
       remove(index); //存在,將要刪除的位置傳進去
	}

	@Override
	public void clear() { //清空
        head.next=null; //回到初始化的狀態
        rear=head;
        size=0;
	}

	@Override
	public String toString() { 
		StringBuilder sb = new StringBuilder();
		sb.append("LinkedList:size="+getSize()+"\n");
		if(isEmputy()) {
			sb.append("[]");
		}else {
			sb.append("[");
			Node p = head;
			while(p.next!=null) {
				p=p.next;
				if(p==rear) {
					sb.append(p.data+"]");
				}else {
					sb.append(p.data+",");
				}
			}
		}
		return sb.toString();
	}
	
	@Override
	public boolean equals(Object obj) {
		if(obj==null) {  //傳進來的是空對象
			return false; //不相等
		}
		if(this==obj) { //傳進來的是自己
			return true;  //一定相等
		} 
		if(obj instanceof LinkedList) { //傳進來的對象是本類
			LinkedList<E> list1 = (LinkedList<E>) obj; //強轉一下保險
			if(this.getSize()==list1.getSize()) { //如果兩個鏈表長度相同
                                //就從兩個鏈表的頭開始遍歷,依次比較內容是否相等
				Node p1 = this.head.next; 
				Node p2 = list1.head.next;
			while(p1!=null&&p2!=null) {
				if(p1.data!=p2.data) { //只要有一個數據不相等
					return false; //就不相等
				}else {
					p1=p1.next;
					p2=p2.next;
				}
			}
			return true;
			}
			return false;
		}
		return false;
	}
}

好了,關於鏈表的操作基本都實現了,鏈表的好處就在於不用擴容,增刪快,但是查找起來非常慢,但是不會浪費多餘的空間。希望這篇文章對你有所幫助,不懂的隨時滴我喔!

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