數據結構——基於java的鏈表實現(真正理解鏈表這種數據結構)


原創不易,如需轉載,請注明出處https://www.cnblogs.com/baixianlong/p/10759599.html,否則將追究法律責任!!!

一、鏈表介紹

1、什么是鏈表?

  • 鏈表是一種物理存儲結構上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。如下圖所示,在數據結構中,a1里面的指針存儲着a2的地址,這樣一個鏈接一個,就形成了鏈表。
    鏈表001.png
    • 相鄰元素之間通過指針鏈接
    • 最后一個元素的后繼指針為NULL
    • 在程序執行過程中,鏈表的長度可以增加或縮小
    • 鏈表的空間能夠按需分配
    • 沒有內存空間的浪費

2、鏈表的優缺點?

  • 優點:

    • 插入和刪除時不需移動其他元素, 只需改變指針,效率高。
    • 鏈表各個節點在內存中空間不要求連續,空間利用率高。
    • 大小沒有固定,拓展很靈活。
  • 缺點:

    • 查找數據時效率低,因為不具有隨機訪問性。

3、鏈表的種類?

  • 有單鏈表、雙向鏈表、循環單鏈表、循環雙鏈表等等。

二、單鏈的實現和相關操作

單鏈01.png

1、鏈表類的創建(以下均已單鏈表為基准)

public class SingleLinkedList {
	//head為頭節點,他不存放任何的數據,只是充當一個指向鏈表中真正存放數據的第一個節點的作用
	public Node head = new Node();  

    //內部類,定義node節點,使用內部類的最大好處是可以和外部類進行私有操作的互相訪問
    class Node{
        public int val;  //int類型會導致head節點的val為0,不影響我們學習
        public Node next;
        public Node(){}
        public Node(int val){
            this.val = val;
        }
    } 

	//下面就可以自定義各種鏈表操作。。。
}

2、鏈表添加結點

//找到鏈表的末尾結點,把新添加的數據作為末尾結點的后續結點
public void add(int data){
    if (head.next == null){
        head.next = new Node(data);
        return;
    }
    Node temp = head;
    while (temp.next != null){
        temp = temp.next;
    }
    temp.next = new Node(data);
}

3、鏈表刪除節點

//把要刪除結點的前結點指向要刪除結點的后結點,即直接跳過待刪除結點
public boolean deleteNode(int index){
    if (index < 0 || index > length() ){
        return false;
    }
    if (index == 1){ //刪除頭結點
        head = head.next;
        return true;
    }
    Node preNode = head;
    Node curNode = preNode.next;
    int i = 2;
    while (curNode!=null){
        if (index == i){
            preNode.next = curNode.next;  //指向刪除節點的后一個節點
            break;
        }
        preNode = curNode;
        curNode = preNode.next;
        i++;
    }
    return true;
}

4、鏈表長度、節點獲取以及鏈表遍歷

//獲取鏈表長度
public int length(){
    int length = 0;
    Node temp = head;
    while (temp.next!=null){
        length++;
        temp = temp.next;
    }
    return length;
}


//獲取最后一個節點
public Node getLastNode(){
    Node temp = head;
    while (temp.next != null){
        temp = temp.next;
    }
    return temp;
}


//獲取第index節點
public Node getNodeByIndex(int index){
    if(index<1 || index>length()){
        return null;
    }
    Node temp = head;
    int i = 1;
    while (temp.next != null){
        temp = temp.next;
        if (index==i){
            break;
        }
        i++;
    }
    return temp;
}


//打印節點
public void printLink(){
    Node curNode = head;
    while(curNode !=null){
        System.out.print(curNode.val+" ");
        curNode = curNode.next;
    }
}

5、查找單鏈表中的倒數第n個結點

//兩個指針,第一個指針向前移動k-1次,之后兩個指針共同前進,當前面的指針到達末尾時,后面的指針所在的位置就是倒數第k個位置
public Node findReverNode(int index){
    if(index<1 || index>length()){
        return null;
    }
    Node first = head;
    Node second = head;
    for (int i = 0; i < index - 1; i++) {
        second = second.next;
    }
    while (second.next != null){
        first = first.next;
        second = second.next;
    }
    return first;
}

6、查找單鏈表中的中間結點

//也是設置兩個指針first和second,只不過這里是,兩個指針同時向前走,second指針每次走兩步,
//first指針每次走一步,直到second指針走到最后一個結點時,此時first指針所指的結點就是中間結點。
public Node findMiddleNode(){
    Node slowPoint = head;
    Node quickPoint = head;
    //鏈表結點個數為奇數時,返回的是中間結點;鏈表結點個數為偶數時,返回的是中間兩個結點中的前個
    while(quickPoint != null && quickPoint.next != null){
        slowPoint = slowPoint.next;
        quickPoint = quickPoint.next.next;
    }
    return slowPoint;
}

7、從尾到頭打印單鏈表

//方法一:先反轉鏈表,再輸出鏈表,需要鏈表遍歷兩次(不建議這么做,改變了鏈表的結構)
。。。
//方法二、通過遞歸來實現(鏈表很長的時候,就會導致方法調用的層級很深,有可能造成StackOverflowError)
public void reservePrt(Node node){
    if(node != null){
        reservePrt(node.next);
        System.out.print(node.val+" ");
    }
}

//方法三、把鏈表中的元素放入棧中再輸出,需要維護額外的棧空間
public void reservePrt2(Node node){
    if(node != null){
        Stack<Node> stack = new Stack<Node>();  //新建一個棧
        Node current = head;
        //將鏈表的所有結點壓棧
        while (current != null) {
            stack.push(current);  //將當前結點壓棧
            current = current.next;
        }
        //將棧中的結點打印輸出即可
        while (stack.size() > 0) {
            System.out.print(stack.pop().val+" ");  //出棧操作
        }
    }
}

8、單鏈表的反轉(1->2->3->4變為4->3->2->1)

//從頭到尾遍歷原鏈表,每遍歷一個結點,將其摘下放在新鏈表的最前端。注意鏈表為空和只有一個結點的情況。時間復雜度為O(n)
public void reserveLink(){
    Node curNode = head;
    Node preNode = null;
    while (curNode.next != null){
        Node nextNode = curNode.next;
		//主要理解以下邏輯
        curNode.next = preNode; //將current的下一個結點指向新鏈表的頭結點
        preNode = curNode;  //將改變了指向的cruNode賦值給preNode
        curNode = nextNode;
    }
    curNode.next = preNode;
    preNode = curNode;
    head = preNode;
}

9、判斷鏈表是否有環

鏈表環.png

//設置快指針和慢指針,慢指針每次走一步,快指針每次走兩步,當快指針與慢指針相等時,就說明該鏈表有環
public boolean isRinged(){
    if(head == null){
        return false;
    }
    Node slow = head;
    Node fast = head;
    while(fast.next != null && fast.next.next != null){
        slow = slow.next;
        fast = fast.next.next;
        if(fast == slow){
            return true;
        }
    }
    return false;
}

10、取出有環鏈表中,環的長度

單鏈環01.png

//獲取環的相遇點
public Node getFirstMeet(){
    if(head == null){
        return null;
    }
    Node slow = head;
    Node fast = head;
    while(fast.next != null && fast.next.next != null){
        slow = slow.next;
        fast = fast.next.next;
        if(fast == slow){
            return slow;
        }
    }
    return null;
}

//首先得到相遇的結點,這個結點肯定是在環里,我們可以讓這個結點對應的指針一直往下走,直到它回到原點,就可以算出環的長度
public int getCycleLength(){
    Node current = getFirstMeet(); //獲取相遇點
    int length = 0;
    while (current != null) {
        current = current.next;
        length++;
        if (current == getFirstMeet()) {  //當current結點走到原點的時候
            return length;
        }
    }
    return length;
}

11、判斷兩個鏈表是否相交

//兩個鏈表相交,則它們的尾結點一定相同,比較兩個鏈表的尾結點是否相同即可
public boolean isCross(Node head1, Node head2){
    Node temp1 = head1;
    Node temp2 = head2;
    while(temp1.next != null){
        temp1 = temp1.next;
    }
    while(temp2.next != null){
        temp2 = temp2.next;
    }
    if(temp1 == temp2){
        return true;
    }
    return false;
}

12、如果鏈表相交,求鏈表相交的起始點

鏈表相交01.png

 /**
 * 如果鏈表相交,求鏈表相交的起始點:
 * 1、首先判斷鏈表是否相交,如果兩個鏈表不相交,則求相交起點沒有意義
 * 2、求出兩個鏈表長度之差:len=length1-length2
 * 3、讓較長的鏈表先走len步
 * 4、然后兩個鏈表同步向前移動,每移動一次就比較它們的結點是否相等,第一個相等的結點即為它們的第一個相交點
 */
public Node findFirstCrossPoint(SingleLinkedList linkedList1, SingleLinkedList linkedList2){
    //鏈表不相交
    if(!isCross(linkedList1.head,linkedList2.head)){
        return null;
    }else{
        int length1 = linkedList1.length();//鏈表1的長度
        int length2 = linkedList2.length();//鏈表2的長度
        Node temp1 = linkedList1.head;//鏈表1的頭結點
        Node temp2 = linkedList2.head;//鏈表2的頭結點
        int len = length1 - length2;//鏈表1和鏈表2的長度差

        if(len > 0){//鏈表1比鏈表2長,鏈表1先前移len步        
            for(int i=0; i<len; i++){
                temp1 = temp1.next;
            }
        }else{//鏈表2比鏈表1長,鏈表2先前移len步
            for(int i=0; i<len; i++){
                temp2 = temp2.next;
            }
        }
        //鏈表1和鏈表2同時前移,直到找到鏈表1和鏈表2相交的結點
        while(temp1 != temp2){
            temp1 = temp1.next;
            temp2 = temp2.next;
        }
        return temp1;
    }
}

13、合並兩個有序的單鏈表(將1->2->3和1->3->4合並為1->1->2->3->3->4)

//兩個參數代表的是兩個鏈表的頭結點
//方法一
public Node mergeLinkList(Node head1, Node head2) {
    if (head1 == null && head2 == null) {  //如果兩個鏈表都為空
        return null;
    }
    if (head1 == null) {
        return head2;
    }
    if (head2 == null) {
        return head1;
    }
    Node head; //新鏈表的頭結點
    Node current;  //current結點指向新鏈表
    // 一開始,我們讓current結點指向head1和head2中較小的數據,得到head結點
    if (head1.val <= head2.val) {
        head = head1;
        current = head1;
        head1 = head1.next;
    } else {
        head = head2;
        current = head2;
        head2 = head2.next;
    }

    while (head1 != null && head2 != null) {
        if (head1.val <= head2.val) {
            current.next = head1;  //新鏈表中,current指針的下一個結點對應較小的那個數據
            current = current.next; //current指針下移
            head1 = head1.next;
        } else {
            current.next = head2;
            current = current.next;
            head2 = head2.next;
        }
    }
    //合並剩余的元素
    if (head1 != null) { //說明鏈表2遍歷完了,是空的
        current.next = head1;
    }
    if (head2 != null) { //說明鏈表1遍歷完了,是空的
        current.next = head2;
    }
    return head;
}


//方法二:遞歸法
public Node merge(Node head1, Node head2) {
        if(head1 == null){
            return head2;
        }
        if(head2 == null){
            return head1;
        }
        Node head = null;
        if(head1.val <= head2.val){
            head = head1;
            head.next = merge(head1.next,head2);
        }else{
            head = head2;
            head.next = merge(head1,head2.next);
        }
        return head;
    }

到此單鏈表的一些常見操作展示的差不多了,如有興趣可繼續深入研究~~~

三、其它種類鏈表(拓展)

1、雙向鏈表(java.util中的LinkedList就是雙鏈的一種實現)

  雙向鏈表(雙鏈表)是鏈表的一種。和單鏈表一樣,雙鏈表也是由節點組成,它的每個數據結點中都有兩個指針,分別指向直接后繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和后繼結點。一般我們都構造雙向循環鏈表。

雙向鏈表.png

  • 優點:對於鏈表中一個給的的結點,可以從兩個方向進行操,雙向鏈表相對單鏈表更適合元素的查詢工作。
  • 缺點:
    • 每個結點需要再添加一個額外的指針,因此需要更多的空間開銷。
    • 結點的插入或者刪除更加費時。

以下是雙鏈的相關實現和操作(其實單鏈弄明白了,雙鏈只不過多維護了個前節點)

雙鏈插入.jpg
雙鏈刪除.jpg
雙鏈尾插.jpg
雙鏈尾刪.jpg

public class DoubleLink<T> {

    // 表頭
    private DNode<T> mHead;
    // 節點個數
    private int mCount;

    // 雙向鏈表“節點”對應的結構體
    private class DNode<T> {
        public DNode prev;
        public DNode next;
        public T value;

        public DNode(T value, DNode prev, DNode next) {
            this.value = value;
            this.prev = prev;
            this.next = next;
        }
    }

    // 構造函數
    public DoubleLink() {
        // 創建“表頭”。注意:表頭沒有存儲數據!
        mHead = new DNode<T>(null, null, null);
        mHead.prev = mHead.next = mHead;
        // 初始化“節點個數”為0
        mCount = 0;
    }

    // 返回節點數目
    public int size() {
        return mCount;
    }

    // 返回鏈表是否為空
    public boolean isEmpty() {
        return mCount==0;
    }

    // 獲取第index位置的節點
    private DNode<T> getNode(int index) {
        if (index<0 || index>=mCount)
            throw new IndexOutOfBoundsException();

        // 正向查找
        if (index <= mCount/2) {
            DNode<T> node = mHead.next;
            for (int i=0; i<index; i++)
                node = node.next;

            return node;
        }

        // 反向查找
        DNode<T> rnode = mHead.prev;
        int rindex = mCount - index -1;
        for (int j=0; j<rindex; j++)
            rnode = rnode.prev;

        return rnode;
    }

    // 獲取第index位置的節點的值
    public T get(int index) {
        return getNode(index).value;
    }

    // 獲取第1個節點的值
    public T getFirst() {
        return getNode(0).value;
    }

    // 獲取最后一個節點的值
    public T getLast() {
        return getNode(mCount-1).value;
    }

    // 將節點插入到第index位置之前
    public void insert(int index, T t) {
        if (index==0) {
            DNode<T> node = new DNode<T>(t, mHead, mHead.next);
            mHead.next.prev = node;
            mHead.next = node;
            mCount++;
            return ;
        }

        DNode<T> inode = getNode(index);
        DNode<T> tnode = new DNode<T>(t, inode.prev, inode);
        inode.prev.next = tnode;
        inode.next = tnode;
        mCount++;
        return ;
    }

    // 將節點插入第一個節點處。
    public void insertFirst(T t) {
        insert(0, t);
    }

    // 將節點追加到鏈表的末尾
    public void appendLast(T t) {
        DNode<T> node = new DNode<T>(t, mHead.prev, mHead);
        mHead.prev.next = node;
        mHead.prev = node;
        mCount++;
    }

    // 刪除index位置的節點
    public void del(int index) {
        DNode<T> inode = getNode(index);
        inode.prev.next = inode.next;
        inode.next.prev = inode.prev;
        inode = null;
        mCount--;
    }

    // 刪除第一個節點
    public void deleteFirst() {
        del(0);
    }

    // 刪除最后一個節點
    public void deleteLast() {
        del(mCount-1);
    }
}

2、循環單鏈表、循環雙鏈表(操作和單鏈、雙鏈是一樣的,不贅述了)

單向循環002.png
雙向循環002.png

四、總結

  • 本文主要是對於鏈表這種數據結構的介紹和認知,明白鏈表的優劣勢。
  • 重點是要學會對於單鏈的操作,體會它的一些獨到之處,至於其它衍生鏈表,舉一反三而已!!!

個人博客地址:

cnblogs:https://www.cnblogs.com/baixianlong
csdn:https://blog.csdn.net/tiantuo6513
segmentfault:https://segmentfault.com/u/baixianlong
github:https://github.com/xianlongbai


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM