線性表——連結串列

cxbf.發表於2020-10-10

連結串列


概念

採用鏈式儲存結構的的線性表稱為連結串列,連結串列中每個結點包含存放資料元素值的資料域和存放指向邏輯上相鄰結點的指標域(地址域),只有一個指標域指向後一個結點的叫做單連結串列,有兩個指標域指向前後兩個結點的叫做雙連結串列。在使用連結串列時只關心邏輯順序而不用在意儲存位置,這是因為連結串列儲存位置是零散的、碎片化的

單連結串列

單連結串列示意圖
單連結串列示意圖

單連結串列的構造

結點初始化

public class Node{
	Object data;				//資料域,儲存資料元素
	Node next;					//地址域,儲存下一個元素的地址,指向後繼
	public Node(){				//構造一個無參的建構函式,可實現初始化一個空的結點
		this(null,null);		
	}
	public Node(Object data){	//構造一個帶一個data引數的構造方法,可實現構造一個資料域為指定引數值而指標域為空的結點
		this(data,null);
	}
	public Node(Object data, Node next){	//構造一個帶倆引數的構造方法,可實現構造一個資料域和指標域都為指定引數值的結點
		this.data = data;
		this.next = next;
	}		
}

頭結點

頭結點用head表示
單連結串列示意圖我們可知連結串列中需要一個頭結點,此頭結點可由結點初始化的無參建構函式來完成

public class LinkList{
	public Node head;
	public LinkList(){
		this.head = new Node();				//呼叫`結點初始化`中的無參建構函式完成對頭結點的建立
	}
	public LinkList(int n,boolean Order,Object[] temp)throws Exception{		//構造一個長度為n的單連結串列
		this();								//置一個只有頭結點的空表
		//後續步驟為呼叫方法建表			
	}
}	

在有一個頭結點的情況下,我們可以用頭插法或尾插法進行建表

頭插法建表

通過呼叫前面方法進行

//頭插法建表基本理念為永遠在頭結點後,首結點前插入新的結點
public void createHead(int n)throws Exception{
	Scanner input = new Scanner(System.in);
	for(int j = 0; j < n; j++)
		insert(0,input.next());
	input.close();
}

自行書寫

 // 頭插法
    public void create2(Object[] temp) throws Exception{        //將Demo中的陣列進行傳遞
        // Node head = this.head;                          //頭結點
        if(temp == null)
            throw new Exception("空物件異常");
        if(temp.length == 0)                            
            System.out.println("傳遞陣列為空");
        for (int i = 0; i < temp.length; i++) {         //利用for迴圈進行逐個插入
            Node node = new Node(temp[i],null);         //建立新結點
            node.next = this.head.next;                              //將原本首結點的地址賦予給新結點
            this.head.next = node;                              //將新結點地址賦予首結點
        }   
    }

尾插法建表

呼叫前面方法完成

public void createRear(int n) throws Exception throws Exception{
        Scanner input = new Scanner(System.in);
        System.out.println("請輸入元素");
        for(int j = 0; j < n; j++)
            insert(length(), input.next());
        input.close();
}

自行書寫

//尾插法基本理念為在最後一個結點後面加新的結點
public void createRear(Object[] temp)throws Exception{
	Node rear = this.head;	//尾指標rear
	if(temp == null)
		throw new Exception("空物件異常");
	if(temp.length == 0)
		System.out.println("連結串列為空");
	for(int i = 0; i < temp.length; i++){
		rear.next = new Node(temp[i], null);
		rear = rear.next;		//尾指標自賦值,指向新的尾結點
	}
}

上述頭插法方法中涉及連結串列的插入方法

單連結串列基本操作

插入

單連結串列的插入操作我們實際上只需要改變兩個地方,需要插入結點位置的前一個結點的地址域,需要插入結點的地址域
插入程式碼塊

public void insert(int i, Object x) throws Exception{
	if(x == null)
		throw new Exception("不能插入空物件");
	Node p = this.head;								//p指向頭結點
	for(int j = 0; p.next != null && j < i; j++)	// 尋找插入位置
		p = p.next;
	p.next = new Node(x, p.next);
}

查詢

單連結串列的查詢我們一般採用遍歷的查詢方法,因為,在連結串列中沒有索引,只有通過從表頭開始一個一個往下尋找,當前結點也只知道它的後繼的地址

按值查詢
public int search(Object key) throws Exception{
        if(key == null)
            throw new Exception("需要尋找元素為空");
        int i = -1;                                                             
        for(Node p = this.head.next; p != null; p = p.next){        //p這裡指向頭結點,不是首結點;p自賦值
            i++;
            if(p.data.equals(key))                                  //p指向結點的資料域與需要尋找元素進行對比
                return i;                                           //返回位置
        }
        throw new Exception("該連結串列無此元素");
    }
按序號查詢
public Object get(int i) throws Exception{
		if(i < 0)									//防禦程式碼
			throw new Exception("序號輸入錯誤!");
        Node p = head.next;                         //初始化,p指向首結點地址域,j為計數器
        int j = 0;
        while(p != null && j < i){					//當p指向結點地址域(當前p指向頭結點)不為空且計數器小於需要查詢需要時進行迴圈
            p = p.next;								//p自賦值後移
            ++j;									//計數器++
        }
        if(j > i || p == null){						//當計數器值大於序號或p指向結點為空(即尾結點)時說明需要查詢元素不存在
            throw new Exception("第" + i + "個元素不存在");
        }
        return p.data;								//返回p的地址域
    }

刪除

當我們需要刪除單連結串列中某個結點時,通常我們需要先找到它,然後再進行刪除操作,連結串列刪除相對於順序表更加簡單快捷,只需要改變地址域指向即可,同順序表中,我們也應對刪除的元素進行儲存

// 刪除帶頭結點的單連結串列中的第i個結點
    public Object remove(int i) throws Exception{
        if(i >= 0 && i < length()){
            Node p = this.head;                 //p目前為頭指標
            //定位到待刪除結點(i)的前驅結點(i - 1)
            for(int j = 0; p.next != null && j < i; j++){
                p = p.next;
            }
            if(p.next != null){
                Object old = p.next.data;//獲取原物件
                p.next = p.next.next;//刪除p的後繼結點
                return old;
            }
        }
        //當i < 0或大於表長時
        return null;
    }

完整程式碼

public class LinkList02 {
    public Node head;

    public LinkList02(){
        this.head = new Node();                             //呼叫結點初始化中的無參建構函式完成對頭結點的建立
    }
    public LinkList02(int n,boolean Order,Object[] temp)throws Exception{     //構造一個長度為n的單連結串列
        this();                                                 //置一個只有頭結點的空表
        if(Order)
            create1(temp);
        else
            create2(temp);
    }
    // 尾插法
    public void create1(Object[] temp)throws Exception{
        Node rear = this.head;	//尾指標rear
        if(temp == null)
            throw new Exception("空物件異常");
        if(temp.length == 0)
            System.out.println("連結串列為空");
        for(int i = 0; i < temp.length; i++){
            rear.next = new Node(temp[i], null);
            rear = rear.next;		//尾指標自賦值,指向新的尾結點
        }
    }
    // 頭插法
    public void create2(Object[] temp) throws Exception{        //將Demo中的陣列進行傳遞
        Node head = this.head;                          //頭指標
        if(temp == null)
            throw new Exception("空物件異常");
        if(temp.length == 0)                            
            System.out.println("傳遞陣列為空");
        for (int i = 0; i < temp.length; i++) {         //利用for迴圈進行逐個插入
            Node node = new Node(temp[i],null);         //建立新結點
            node = head.next;                              //將原本首結點的地址賦予給新結點
            head.next = node;                              //將新結點地址賦予首結點
        }
        
            
    }
    //將一個已經存在的帶頭結點單連結串列製成空表
    public void clear(){
        head.data = null;
        head.next = null;
    } 
    // 判斷帶頭結點的單連結串列是否為空
    public boolean isEmpty(){
        return head.next == null;
    }
    // 求帶頭結點的單連結串列的長度
    public int length(){
        Node p = head.next;                 //初始化,p指向首結點,length為計數器
        int length = 0;                     
        while(p != null){                   //從首結點開始向後查詢,直到p為空
            p = p.next;                     //指向後繼結點
            ++length;                       //長度增1
        }
        return length;
    }

    //按值查詢
    public int search(Object key) throws Exception{
        if(key == null)
            throw new Exception("需要尋找元素為空");
        int i = -1;                                                             
        for(Node p = this.head.next; p != null; p = p.next){        //p這裡指向頭結點,不是首結點;p自賦值
            i++;
            if(p.data.equals(key))                                  //p指向結點的資料域與需要尋找元素進行對比
                return i;                                           //返回位置
        }
        throw new Exception("該連結串列無此元素");
    }


    
    // 讀取帶頭結點的單連結串列中的第i個結點
    public Object get(int i) throws Exception{
        if(i < 0)									//防禦程式碼
			throw new Exception("序號輸入錯誤!");
        Node p = head.next;                         //初始化,p指向首結點,j為計數器
        int j = 0;
        while(p != null && j < i){
            p = p.next;
            ++j;
        }
        if(j > i || p == null){
            throw new Exception("第" + i + "個元素不存在");
        }
        return p.data;
    }
    // 在帶頭結點的單連結串列中的第i個結點之前插入一個值為x的新節點
    public void insert(int i, Object x) throws Exception{
        if(x == null)
            throw new Exception("不能插入空物件");
        Node p = this.head;								//p指向頭結點
        for(int j = 0; p.next != null && j < i; j++)	// 尋找插入位置
            p = p.next;
        p.next = new Node(x, p.next);
    }
    // 刪除帶頭結點的單連結串列中的第i個結點
    public Object remove(int i) throws Exception{
        if(i >= 0 && i < length()){
            Node p = this.head;                 //p目前為頭指標
            //定位到待刪除結點(i)的前驅結點(i - 1)
            for(int j = 0; p.next != null && j < i; j++){
                p = p.next;
            }
            if(p.next != null){
                Object old = p.next.data;//獲取原物件
                p.next = p.next.next;//刪除p的後繼結點
                return old;
            }
        }
        //當i < 0或大於表長時
        return null;
    }
    // 在帶頭結點的單連結串列中查詢值為x的結點
    public int indexOf(Object x){
        Node p = head.next;                             //初始化,p指向首結點,j為計數器
        int j = 0;
        // 下面從單連結串列中的首結點開始查詢,直到p.data為x或到達單連結串列的表尾
        while(p != null && !p.data.equals(x)){
            p = p.next;                                 //指向下一個結點
            ++j;                                        //計數器+1
        }
        if(p != null)
            return j;                                   //返回值為x的結點在單連結串列中的位置
        else
            return -1;                                  //值為x的結點不在單連結串列中,則返回-1
    }
    // 輸出單連結串列中的所有結點
    public void display(){
        Node node = head.next;                          //取出帶頭結點的單連結串列中的首結點
        while(node != null)                             
        {
            System.out.print(node.data + " ");        //輸出結點的值
            node = node.next;                           //取下一個結點
        }
        System.out.println();                           //換行
    }

    //迴圈連結串列的實現
    public void loop(Object[] temp){
        Node rear = this.head;
        for(int i = 0; i < temp.length; i++){
            rear.next = new Node(temp[i], null);
            rear = rear.next;		//尾指標自賦值,指向新的尾結點
        }
        rear.next = this.head.next;
    }
}

public class Node {
   Object data;                         //資料域
   Node next;                           //地址域
   public Node(){                       //構造一個無參的建構函式,可實現初始化一個空的結點
        this(null,null); 
   }
   public Node(Object data){            //構造一個帶一個data引數的構造方法,可實現構造一個資料域為指定引數值而指標域為空的結點
        this(data,null);
   }
   public Node(Object data,Node next){  //構造一個帶倆引數的構造方法,可實現構造一個資料域和指標域都為指定引數值的結點
        this.data = data;
        this.next = next;
   }  
}

單連結串列約瑟夫環

類同於順序表約瑟夫環問題,程式碼如下

public class Josephus02 {
	public static void count(int n){
		//數到3出局,中間間隔兩個人
		int k = 3;
		//頭結點不儲存資料
		Node head = new Node();
		Node cur = head;
		//迴圈構造這個連結串列
		for(int i=1; i<=n; i++){
			Node node = new Node(i);
			cur.next = node;
			cur = node;
		}
		//連結串列有資料的部分首尾相連形成一個環。
		cur.next = head.next;
		//統計開始的時候,刨去頭結點,然後從第一個有資料的結點開始報數
		Node p = head.next;
		//迴圈退出的條件是最後只剩一個結點,也就是這個結點的下一個結點是它本身
		while(p.next!=p){
			//正常報數的遍歷邏輯
			for(int i=1;i<k-1;i++){
				p = p.next;
			}
			//當數到3的時候,出局
			System.out.print(p.next.data+",");
			p.next = p.next.next;
			p = p.next;
		}
        //最後剩下的一個結點
        System.out.println();
		System.out.println("最後贏家:"+p.data);
	}

	public static void main(String[] args) {
		count(8);
	}
 
}
 
class Node{
	int data;
	Node next;
	public Node(){}
	public Node(int data){this.data = data;}
}

其他連結串列

迴圈連結串列

即環形連結串列,結構與單連結串列相似。即單連結串列的最後一個結點的後繼指標指向第一個結點,從而構成一個環形連結串列,繼而此連結串列每一個結點都有後繼,地址域均不為空

Node p = tailb.next;				//p指向第二個表的頭結點
tailb.next = taila.next;			//第二個表的表尾與第一個表的表頭相連線
taila.next = p.next;				//第一個表的表尾與第二個表的首結點相連

迴圈連結串列一般會採用尾指標,因為在實際中,採用尾指標找迴圈連結串列最後一個結點和第一個結點時演算法複雜度都為O(1)
迴圈連結串列的實現

//迴圈連結串列的實現
    public void loop(Object[] temp){
        Node rear = this.head;
        for(int i = 0; i < temp.length; i++){
            rear.next = new Node(temp[i], null);
            rear = rear.next;		//尾指標自賦值,指向新的尾結點
        }
        rear.next = this.head.next;	//使尾結點指標域指向頭結點
    }

雙向連結串列

雙向連結串列不同於單連結串列,它擁有兩個指標域,分別指向其前驅和後繼
雙連結串列與單連結串列只在插入和刪除上有較大差異,其他基本與單連結串列無異
下面給出完整程式碼
方法類

import java.util.Scanner;

public class DuLinkList {
    DuLNode head;
    
    public DuLinkList(){
        head = new DuLNode();
        head.prior = head;
        head.next = head;
    }

    //雙向連結串列構建
    public DuLinkList(int n) throws Exception{
        this();
        Scanner input = new Scanner(System.in);
        for (int j = 0; j < n; j++)
            insert(0,input.next());
        input.close();
    }

    // 插入
    public void insert (int i, Object x) throws Exception{
        DuLNode p = head.next;              //指標p指向頭結點
        int j = 0;                          //計數器
        while( !p.equals(head) && j < i){   //當p不等於頭指標本身(指向本身則說明為空表且計數器效於插入位置時
            p = p.next;                     //自賦值
            ++j;                            //計數器++
        }
        if(j != i && p.equals(head))        //防禦程式碼
            throw new Exception("插入位置不合法");
        DuLNode s = new DuLNode(x);         //新結點
        p.prior.next = s;                   //將s的地址賦給p前結點的後指標域
        s.prior = p.prior;                  //將原p前面一個結點的地址賦給s的前指標域
        s.next = p;                         //將p的地址賦給s的後指標域
        p.prior = s;                        //將s的地址賦給p的前指標域
    }

    //刪除
    public void remove(int i ) throws Exception{
        DuLNode p = head.next;                  //p指向頭結點
        int j = 0;                              //計數器
        while( !p.equals(head) && j < i){       
            p = p.next;
            ++j;
        }
        if(j != i)
            throw new Exception("刪除位置不合法");
        p.prior.next = p.next;                  //p指向的前一個結點的後指標域指向p的後一個結點
        p.next.prior = p.prior;                 //p指向的後一個結點的前指標域指向p的前一個結點
    }
    //將一個已經存在的帶頭結點雙連結串列製成空表
    public void clear(){
        head.data = null;
        head.next = null;
        head.prior = null;
    }
    //判斷帶頭結點的雙連結串列是否為空
    public boolean isEmpty(){ 
        return head.next == null;
    }
    //讀取帶頭結點的雙連結串列中的第i個結點
    public Object get(int i)throws Exception{
        DuLNode p = head.next;          //初始化頭結點指標域
        int j;
        for( j = 0; p != null && j < i; ++j)
            p = p.next;
        if(j > i || p == null)
            throw new Exception("第" + i + "個元素不存在");
        return p.data;
    }
    //獲取帶頭結點的雙連結串列的長度
    public int length(){
        DuLNode p = head.next;          //初始化,p指向頭結點
        int length = 0;                 //表長計數器
        if(p == null){
            return length;
        }
        while(!p.equals(head)){             //從首結點開始向後查詢,直到p為空
            p = p.next;                     //指向後繼結點
            ++length;                       //長度增1
        }
        //     ++length;                 //自賦值
        return length;                  //返回長度
    }
    //在帶頭結點的雙連結串列中查詢值為x的結點
    public int indexOf(Object x){
        DuLNode p = head.next;                             //初始化,p指向首結點,j為計數器
        int j = 0;
        // 下面從單連結串列中的首結點開始查詢,直到p.data為x或到達單連結串列的表尾
        while(p != null && !p.data.equals(x)){
            p = p.next;                                 //指向下一個結點
            ++j;                                        //計數器+1
        }
        if(p != null)
            return j;                                   //返回值為x的結點在單連結串列中的位置
        else
            return -1;                                  //值為x的結點不在單連結串列中,則返回-1
    }
    //輸出所有結點
    public void display(){
        DuLNode node = head.next;
        while( !node.equals(head)){
            System.out.print(node.data + " ");
            node = node.next;
        }
        System.out.println();
    }
}

結點構造

public class DuLNode {
    Object data;        //資料域
    DuLNode prior;      //前驅結點
    DuLNode next;       //後繼節點
    public DuLNode(){ 
        this(null,null,null);
    }
    public DuLNode(Object data){
        this(data, null, null);
    }
    public DuLNode(Object data, DuLNode prior){
        this(data,prior,null);
    }
    //構造資料域值為data的結點
    public DuLNode(Object data, DuLNode prior, DuLNode next) {
        this.data = data;
        this.prior = prior;
        this.next = next;
    }
}

注意(待解決)

在雙連結串列中使用遍歷似乎只能用!p.equals(head),若使用p != null判斷條件進行遍歷時會出現死迴圈,而單連結串列中則不會出現這個錯誤,但是若單連結串列使用!p.equals(head)會出現空指標異常…

順序表與連結串列比較

順序表隨機存取效率高,但是插入和刪除效率較慢在時間複雜度上查詢O(1);插入和刪除O(n),需要預先分配空間,容易出現溢位問題
連結串列插入和刪除效率高,但查詢速度慢,原理上沒有空間限額,可以無上限儲存,實際上與空間大小有關,查詢O(n);刪除O(1)。
所以我們 在採用時應當根據實際情況而定。

相關文章