數據結構——鏈式存儲結構(鏈棧、鏈式隊列、循環鏈表)

鏈表的基礎內容在上篇已經介紹完了,現在介紹鏈棧的內容,棧的先進後出的特點我就不再贅述了。在介紹用順序表實現棧結構的時候,已經對棧進行了一個完整的介紹,關於用結點去實現一個棧結構在Java基礎的文章中也已經介紹過了,請參照文章(

https://blog.csdn.net/weixin_45432742/article/details/99850913),在這裏介紹的是用上篇文章中已經實現的LinkedList<E>中的方法來實現鏈棧的結構,進行壓棧(頭插)和彈棧(頭刪)操作。下面是它的類圖和方法的實現:

package 線性棧;

import 線性表.LinkedList;

public class LinkedStack<E> implements Stack<E>{

	private LinkedList<E>  list; //用鏈表已實現的方法實現鏈棧,創建鏈表的引用
	
	public LinkedStack() { //鏈棧的構造方法
		list = new LinkedList<E>(); //創建一個鏈表的對象
	}
	@Override
	public int getSize() { //獲取棧的容量
		return list.getSize(); //調用鏈表的getSize方法獲取鏈表的有效元素
	}

	@Override
	public boolean isEmputy() { //判斷棧是否爲空
		return list.isEmputy(); //那就判斷此時鏈表是否爲空
	}

	@Override
	public void push(E e) { //進棧/壓棧操作
		list.addFirst(e); //進棧元素總是在前一個進棧元素的前面或者上面,因此採用頭插法
                                 //調用鏈表的在頭添加結點的方法
	}

	@Override
	public E pop() { //出棧/彈棧
		return list.removeFirst(); //彈棧是將最上面/最前面的元素刪除,因此調用頭刪的方法
	}

	@Override
	public E peek() { //獲取棧頂元素
		return list.getFirst(); //獲取鏈表的頭結點的數據
	}

	@Override
	public void clear() { //清空棧
		list.clear(); //清空當前的鏈表
	}

	@Override
	public String toString() { 
		StringBuilder sb = new StringBuilder();
		sb.append("LinkedStack:size="+getSize()+"\n");
		if(isEmputy()) {
			sb.append("[]");
		}else {
			sb.append('[');
			for(int i=0;i<getSize();i++) {
				sb.append(list.get(i));
				if(i!=getSize()-1) {
					sb.append(',');
				}else {
					sb.append(']');
				}
			}
		}
		return sb.toString();
	}
	
	@Override
	public boolean equals(Object obj) { //判斷兩個棧是否相等
		if(obj==null) { //傳入的棧對象爲空
			return false; //不相等
		}
		if(this==obj) { //傳入的是自己
			return true; //相等
		}
		if(obj instanceof LinkedStack) { //傳入的是鏈棧對象
			LinkedStack<E> list1 = (LinkedStack<E>) obj; //將它強轉一下
			return list.equals(list1.list); //然後用真實面目也是棧裏面的鏈表對象調用鏈表的equals方法去比較兩個鏈表是否相等
		}
		return false;
	}
}

鏈棧內容簡單,就不過多的去介紹了,只要明白棧的結構和特點,基本上不管是用數組亦或者是鏈表都能自己編程實現。在這篇文章裏,我想重點的介紹的是循環鏈表的內容。這是比用數組實現的循環隊列還麻煩的操作。而且涉及到用循環鏈表解決的問題,真的也非常的麻煩,希望你在看這篇文章的時候,一定要思路清晰、耐心加上較好的理解性。我都能學會,相信翻到這篇文章的你,也一定可以(默默的在心裏暗示,我是最棒的!)。好,準備好了之後,我們進行接下來的內容。

隊列

用鏈表實現的隊列和用順序結構實現的隊列的思想是一樣的,是先進先出的隊列,與棧相反,所以普通隊列也可以用已實現的鏈表類去實現,其特殊操作就在於入隊(尾插)和出隊(頭刪),如果不太能想的到其具體的操作的話,可以翻看上一篇文章鏈表的尾插操作圖和頭刪操作圖去理解。在這裏我還是不贅述隊列是什麼以及怎麼進行入隊和出隊的具體操作了(在上篇文章裏都有),我想把重點的內容放在循環鏈表裏,因此這裏直接給出普通隊列的類圖和代碼實現(有註釋的,應該可以看得懂):

package 線性隊列;

import 線性表.LinkedList;

public class LinkedQueue<E> implements Queue<E>{

	private LinkedList<E> list; //還是採用已經實現的鏈表去實現隊列結構
	
	public LinkedQueue() {
		list = new LinkedList<E>(); //初始化創建一個鏈表對象
	}
	@Override
	public int getSize() { //獲取隊列的長度
		return list.getSize(); //仍然獲取的是鏈表的有效結點個數
	}

	@Override
	public boolean isEmpty() { //判斷隊列是否爲空
		return list.isEmputy(); //判斷內層鏈表是否爲空
	}

	@Override
	public void clear() {  //清空隊列
        list.clear();	 //將內層鏈表清空	
	}

	@Override
	public E dequeue() { //出隊操作
		return list.removeFirst(); //將鏈表的頭結點刪除
	}

	@Override
	public E getFront() { //獲取隊頭元素
		return list.getFirst(); //獲取的是鏈表的第一個結點(真實的頭結點的數據)
	}

	@Override
	public E getRear() { //獲取隊尾元素
		return list.getLast(); //獲取的是鏈表尾結點的數據
	}

	@Override
	public void enqueue(E e) { //入隊操作
         list.addFirst(e);  //在鏈表的真實頭結點和虛擬頭結點處添加一個結點	
	}

	@Override
	public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("LinkedQueue:size="+getSize()+"\n");
        if(isEmpty()) {
        	sb.append("[]");
        }else {
        	sb.append('[');
        	for(int i=0;i<getSize();i++) {
        		sb.append(list.get(i));
        		if(i==getSize()-1) {
        			sb.append(']');
        		}else {
        			sb.append(',');
        		}
        	}
        }
		return sb.toString();
	}
	
	@Override
	public boolean equals(Object obj) { //判斷兩個隊列是否相等
		if(obj==null) {
			return false;
		}
		if(this==obj) {
			return true;
		}
		if(obj instanceof LinkedQueue) {
			LinkedQueue<E> list1 = (LinkedQueue<E>) obj; //先把傳進來的隊列進行強轉一下
			return list.equals(list1.list); //然後調用該隊列的內層鏈表對象比較兩個內層鏈表的內容是否相等
		}
		return false;
	}
}

一個普通的隊列結構已經實現,其實在理解上並不是很難,代碼量也不是很大,因此鏈棧和普通隊列相對來說比較簡單的,可能有朋友會想,爲什麼沒有雙端棧和雙端隊列呢?因爲我們現在介紹的是單向鏈表,每個結點只存儲下一個結點的地址,不能反向進行查找,因此目前還不能實現雙端棧的結構,在今後學習雙向鏈表的時候,我會在文章中實現這一部分的內容。目前基礎先到這爲止吧,再往下加深難度的話就是循環鏈表的內容了(彆着急,內容如下!)。

循環鏈表

在開始循環鏈表介紹之前,我想說的是(還是要叭叭兩句才能進入正題):不論是鏈表還是棧,亦或者是隊列,都存在一個虛擬的頭結點,但是在實現循環鏈表的結構時,採用的是真實頭結點的鏈表結構實現的。因此這裏就不能再用已經實現的鏈表類去實現循環鏈表了,需要用結點一個一個自己去搭建實現。

在順序存儲結構的循環隊列中,我們採用的是下標循環的方法,而在鏈式存儲結構中,只是把一個單向的直直的單向鏈表,進行了頭尾相接而已,也就是原本的尾結點的指針域本來是不存儲任何地址的,先在存儲的是頭結點的地址,如下圖所示:

這樣就形成了一個單向的循環鏈表啦。那麼讓我們具體的瞭解一下循環鏈表的操作吧!

                                                      

                           

                 

一些特殊的操作我用圖簡單的描述了一下,具體的代碼實現,我會在下面呈上,先別急,我知道我很囉嗦,但是咱還是得慢慢來。來來來,先看看類圖:

循環鏈表也是鏈表,實現的還是List接口,因爲不能用已經實現的普通單向鏈表實現單向循環鏈表,所以在這裏需要我們自己去實現,操作Node結點。(好啦好啦,我不叭叭了,代碼呈上,您過目!)

package 循環鏈表;

import 線性表.LinkedList;
import 線性表.List;

public class LoopSingle<E> implements List<E>{ //注意這裏實現的仍然是我們自己寫的List接口,注意導包不要導錯了

       //節點類我放在最後了
	private Node head; //頭指針
	private Node rear; //尾指針
	private int size;  //有效元素個數
	
	public LoopSingle() { //初始化循環鏈表
		head=null; //頭指針爲空,注意連虛擬頭結點都沒有
		rear=null; //爲指針也爲空
		size=0; //有效元素個數爲零
	}
	public LoopSingle(E[] arr){ //這裏如果傳進來一個數組的話,要把這個數組轉化成循環鏈表
		this(); //先進行初始化
		for(E e:arr) { //然後遍歷數組將數組每個元素進行尾插
			addLast(e);
		}
	}
	
	@Override
	public int getSize() { //返回當前循環鏈表的總長
		return size; //也就是有效元素的個數
	}

	@Override
	public boolean isEmputy() { //判斷當前的循環鏈表是否爲空
		return size==0&&head==null&&rear==null; //其實只用判斷當前有沒有元素即可,多加一重判斷保險一些
	}
	
	@Override
	public void add(int index, E e) { //添加一個結點
		if(index<0||index>size){ //首先判斷添加的指定位置是否在合法的範圍內
			throw new IllegalArgumentException("插入角標非法!");
		}
		Node n=new Node(e,null); //在合法的範圍內,先創建一個結點,把元素存儲在這個結點的數據域中,指針域先置空
		if(isEmputy()){	//特殊情況,如果當前添加的是循環鏈表的第一個結點
			head=n; //那麼頭指針指向的是這個結點的地址
			rear=n; //尾指針也指向的是這個結點的地址,請參照上面的圖
			rear.next=head; //然後此結點的指針域存儲的下一個結點的地址是自己,形成環路
		}else if(index==0){//如果插入的位置在頭部
			n.next=head; //那麼次結點的指針域指向的是當前的頭結點
			head=n; //然後取代了原頭結點的位置成爲現在的頭結點
			rear.next=head; //自然,現在尾結點的指針域指向得是現在的頭結點的地址,形成循環
		}else if(index==size){//在尾部插入元素
			n.next=head; //尾插取代的是當前的尾結點,尾結點的指針始終指向頭結點(當然是在有節點的情況下)
			rear.next=n; //然後把這個結點添加在當前尾結點的後面,取代原尾結點
			rear=n; //尾指針指向新添加的結點
		}else{	//一般情況,在中間插入一個結點
			Node p=head;//首先從頭開始查找
			for(int i=0;i<index-1;i++){ //找到插入位置的前一個結點
				p=p.next;
			}
			n.next=p.next; //將找到的這個結點的下一個結點的地址先給新節點的指針域
			p.next=n; //然後新節點再取代這個結點的位置
		}
		size++; //不管咋加,元素都是在增加的
	}

	@Override
	public void addFirst(E e) { //頭插
		add(0,e); //傳入角標0
	}

	@Override
	public void addLast(E e) { //尾插
		add(size,e); //傳入角標size
	}

	@Override
	public E get(int index) { //獲取相應位置下的結點數據
		if(index<0||index>=size){ //首先還是判斷要獲取的位置是否在合法範圍內
			throw new IllegalArgumentException("查找角標非法!");
		}
		if(index==0){ //如果想要獲取的是頭結點
			return head.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); //傳入角標size-1
	}

	@Override
	public void set(int index, E e) { //修改某個位置結點的數據
		if(index<0||index>=size){ //還是要判斷要修改的位置結點合法不
			throw new IllegalArgumentException("修改角標非法!");
		} //如果合法再往下判斷
		if(index==0){ //如果修改的是頭結點的數據
			head.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) { //查找某個元素在當前循環鏈表的位置下標
		if(isEmputy()){ //先判空,鏈表爲空找個啥?
			return -1;
		}
		Node p=head; //不爲空從頭找
		int index=0; //先設置標記位變量
		while(p.data!=e){ //如果此時結點的數據不爲我們要找的數據,就進入循環遍歷後一個結點數據
			p=p.next;
			index++; //每走一個結點相應的位置要加一
			if(p==head){ //如果又走回到我們的頭結點位置,表示找了一圈都沒找到
				return -1; //那肯定是不存在的
			}
		}
		return index; //如果不是在循環裏面強制返回結束,那麼就是有條件不滿足循環,說明就有,返回此時的標記變量即可
	}

	@Override
	public E remove(int index) { //刪除某個位置的結點
		if(index<0||index>=size){ //仍然要判斷刪除的位置在鏈表裏面不
			throw new IllegalArgumentException("刪除角標非法!");
		} //如果在
		E res=null;//先定義一個變量用來存儲要刪除結點的數據
		if(size==1){	//特殊情況,如果刪除的結點是整個循環鏈表的最後一個結點
			res=head.data; //那麼肯定是頭結點,將頭結點的數據先存儲
			head=null; //然後將頭尾指針置空
			rear=null;
		}else if(index==0){ //如果刪除的是頭結點
			res=head.data; //將頭結點的數據進行存儲
			head=head.next; //然後老二上位,頭結點後面的一個結點取代頭結點的位置,頭指針後移
			rear.next=head; //將此時的新頭結點的地址給尾結點的指針域
		}else if(index==size-1){ //如果刪除的是尾結點
			res=rear.data; //先將尾結點的數據進行存儲
			Node p=head; //其次我們要找到尾結點的前一個結點,來取代尾結點的位置
			while(p.next!=rear){
				p=p.next;
			}
			p.next=rear.next; //找到之後將頭結點的地址存儲到尾結點的前一個結點的指針域裏
			rear=p; //然後將這個結點取代結點作爲當前新的尾結點
		}else{  //如果要刪除的是中間的位置
			Node p=head; //從頭遍歷(這句話我要說吐了)
			for(int i=0;i<index-1;i++){ //找到要刪除結點的前一個結點
				p=p.next;
			}
			Node del=p.next; 將找到的結點的下一個結點的數據存儲
			res=del.data; 
			p.next=del.next;//然後將刪除的結點的下一個結點地址給要刪結點的前一個結點的指針域
		}
		size--; //不管咋刪,鏈表在縮短,元素個數在減少
		return res; //將存儲的數據返回
	}

	@Override
	public E removeFirst() { //獲取頭結點數據
		return remove(0); //傳入角標0
	}

	@Override
	public E removeLast() { //獲取尾結點的數據
		return remove(size-1); //傳入角標size-1,也就是最後一個結點的位置
	}

	@Override
	public void removeElement(E e) { //根據元素刪元素所在的結點
		remove(find(e)); //將找到的元素下標傳給刪除函數
	}

	@Override
	public void clear() { //清空,回到初始化位置
		head=null;
		rear=null;
		size=0;
	}
	@Override
	public String toString() {
		StringBuilder sb=new StringBuilder();
		sb.append("LoopSingle:size="+getSize()+"\n");
		if(isEmputy()){
			sb.append("[]");
		}else{
			sb.append('[');
			Node p=head;
			while(true){
				sb.append(p.data);
				if(p.next==head){
					sb.append(']');
					break;
				}else{
					sb.append(',');
				}
				p=p.next;
			}
		}
		return sb.toString();
	}

        @Override
	public boolean equals(Object obj) { //判斷兩個循環鏈表是否相等
		if(obj==null) {
			return false;
		}
		if(this==obj) {
			return true;
		}
		if(obj instanceof LoopSingle) {
			LoopSingle<E> loop = (LoopSingle<E>) obj;
			if(this.getSize()==loop.getSize()) { //如果兩個循環鏈表的結點個數一樣
				Node p1 = this.head;
				Node p2 = loop.head;
                        //比較裏面的元素是否一一相對應的相等
			while(true) {
				if(p1.data!=p2.data) { //如果存在一個位置對應的結點數據不相等
					return false; //就不相等
				}else { //相等就比較下一個結點的內容
					p1=p1.next;
					p2=p2.next;
					if(p1==head&&p2==head) {
						break; //如果遍歷到頭了,循環正常結束
					}
				}
			}
			return true; //那麼一定是相等的
			}
			return false; 
		}
		return false;
	}
	private class Node{
		E data;		//數據域
		Node next;	//指針域
		public Node(){
			this(null,null);
		}
		public Node(E data,Node next){
			this.data=data;
			this.next=next;
		}
		@Override
		public String toString() {
			return data.toString();
		}
	}
}

至此單向鏈表的內容就介紹到這,今天登陸博客的時候有一哥們兒給我指出來錯誤,很開心有人能那麼認真的看我寫的東西,我一點會勤奮寫博客,可能有時候我自己沒有把寫出來的東西進行調試,所以有錯誤就沒發現。但是我覺得思路清晰就好了,一定一定要思路是清晰的,就像指出我錯誤的哥們兒,他的思路肯定特別清晰,才能發現錯誤。在這裏謝謝你啦啊。所以還是希望大家多多發表自己的意見,我怕有錯誤就會耽誤了人家思路。說聲對不起啦!>-<!

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