鏈表(1) --- 單鏈表


  鏈表,和數組一樣,也是一種線性的數據結構。但鏈表在存儲數據的時候,卻不像數組把所有的數據都存儲在一片連續的內存空間中,而是數據分散在內存中,數據和數據之間相互鏈接。數據和數據怎么才能相互鏈接?比如,5和10怎么才能進行鏈接?很顯然,僅僅依靠數據本身是無法鏈接起來的,還需要地址。存儲數據的時候,同時存儲一個地址,當再存儲一個數據的時候,把該數據的地址賦值前一個數據中的地址,只要知道地址,就能找到元素,也就相當於,數據和數據鏈接起來了,所以鏈表在存儲數據的時候,不僅要存數據本身,還要存地址。也正因為如此,鏈表存儲的是一個對象。對象的一個屬性是數據,一個屬性是地址。鏈表是對象與對象之間的鏈接。但地址怎么獲取呢?只要鏈表存儲的是對象,地址就很好獲取了。在Java 和JavaScript中,當創建一個對象,並把這個對象賦給一個變量的時候,這個變量保存的是就這個對象的地址。當把這個變量復制給另外一個變量的時候,復制的是對象的地址,也就是說,兩個變量同時指向了一個對象。也就是說,鏈表中的每一個對象中,它的地址屬性只要聲明成對象類型的變量就可以了。鏈表是對象與對象的鏈接,這也引出了另外一個問題,鏈表是存儲在堆內存中的(因為對象都是存儲在堆內存中的),怎么才能操作它呢?只能把鏈表中的第一個對象賦值給一個變量,操作鏈表的時候,通過這個變量去找到鏈表中的第一個對象,進而找到整個鏈表進行操作。也就是說,鏈表中始終都要存在一個指向鏈表第一個對象的變量,用來找到鏈表,進而操作鏈表。鏈表中的每一個對象也稱為節點, 指向鏈表第一個對象的變量稱為鏈表頭。一個簡單的鏈表如下:

  鏈表的分類有三種,單向鏈表(單鏈表),雙向鏈表,循環鏈表。

  單向鏈表:鏈接方向是單向的,一個節點只能鏈接它的后一個節點,而不能鏈接它的前一個節點,鏈表的最后一個節點鏈向null。對鏈表的訪問只能從頭部開始,依次向后順序訪問,像上圖一樣。

  雙向鏈表,鏈接方向是雙向的,就是它的每一個節點中都有兩個地址(假設是prev和next),prev指向它的前一個節點, next指向后一個節點,第一個節點的prev提向null,最后一個節點next指向null

  

  循環鏈表就是,鏈表的最后一個元素不是指向null,而是指向第一個結點。不管是單向鏈表還是雙向鏈表,它都能變成循環列表

   

 鏈表的實現方式也有兩種方式,一種是不帶頭結點,鏈表頭指向的第一個元素就是真實的數據節點

  一種是帶一個頭節點,頭節點並不保存任何有效數據,鏈表頭指向頭節點,頭節點指向鏈表中真正有意義的第一個節點。

   先看單向鏈表,由於鏈表中的每一個節點都是對象,所以要聲明一個類來表示這個對象,由於這個類僅在單向鏈表中使用,可以聲明為私有內部類,類的屬性分為數據和地址,為了簡單起見,數據使用整數,地址就是節點類的變量。為了能夠操作鏈表,還需要一個變量,讓它指向鏈表中的第一個節點,所以一個單向鏈表要有兩個屬性,一個是內部節點類,一個是指向鏈表的第一個元素。有時還要計算鏈表中節點的個數,再加上一個屬性size

public class SingleLinkedList {

    private class Node { // 鏈表的節點類型
        int data; // 數據
        Node next; // 地址

        Node(int data){
            this.data = data;
        }
    }

    private Node head; // 指向鏈表的第一個元素,用來找到鏈表,操作鏈表
    private int size; // 記錄節點的個數
}

  可以想到,這樣的實現方式是不帶頭節點的,因為head默認初始化為null,它並沒有指向一個節點。

  插入節點,可以從鏈表的頭部插入,可以從鏈表的尾部插入,還可以從鏈表的中間插入,還可以在某個節點后面插入,相應的,你可以單獨提供方法,也可以提供通用的方法。頭部插入最為簡單,就是創建新的節點,讓新的節點next指向head(把head賦值給新節點的next屬性),然后再讓head指向新的接點(把新節點賦值給head),最后不要忘記size++;

public void insertFirst(int data){
    // 創建新的節點
    Node newNode = new Node(data);// 新的節點的next指向head
    newNode.next = head;
    // 讓head指向新的節點
    head = newNode;

size++; }

  從尾部插入節點,就有點麻煩了,因為要先找到尾節點,然后再讓尾節點的next 指向新的節點。怎樣才能找到尾節點?尾節點的next 是null,只要找到一個節點的next 屬性是null, 它就是尾節點。如果head.next 是null,則head是尾節點,如果head.next.next 是null,那head.next 就是尾節點。找一個臨時變量temp, 把head賦值給它, 那么temp 也就是指向了第一個節點,判斷temp.next 是不是null, 如果是,它就是尾節點,如果不是,那就把temp.next 再賦值給temp, 這時temp就指向第二個節點。如果temp.next 還不是null, 那就再把temp.next 賦值給temp, 這時temp指向第三個節點。 可以發現,就是不停地把temp.next 賦值給temp,讓temp 指向下一個節點,直到temp.next 是null,那temp 就是最后一個節點,這時把新的節點賦值給temp.next。賦值成功后,不要忘記size++。尾部插入節點要注意的是,先判斷head是否為null,如果head是null,head賦值給temp,temp.next就會報錯。head為null,鏈表為空,也沒有必要循環鏈表。

 

public void insertLast(int data) {
    // 新node的next默認是null
    Node newNode = new Node(data);

    if (head == null){
        head = newNode;
    } else{
        Node temp = head;
        // 只有node的next的節點是null, 才是最后一個節點。
        while (temp.next != null) {
            temp = temp.next;
        }
        // temp是最后一個節點
        temp.next = newNode;
    }
size++; }

  在鏈表的中間位置或任意位置插入節點,那就要先定位到這個位置,找到這個位置的前一個節點,因為單鏈表只能鏈接到下一個節點,要在某個位置插入節點,肯定要讓這個位置之前的那個節點的next指向新的節點,同時讓新的節點的next指向原來前一個節點指向的后一個節點。定位位置,找到前一個節點,肯定是要循環遍歷了,遍歷到指定的位置。插入到第n個位置,遍歷n-1次,得到的節點,就是前一個節點。

  理論上是這樣,但實現上還要復雜,因為有好多情況要判斷,鏈表是否為空,如果為空,那就只能插入第一個節點。插入的位置是否符合要求,這里要確定一下,鏈表的第 一個節點是位置0還是位置1,如果按照數組的規則,鏈表中的第一個節點就是位置0。那么要插入的位置范圍就是[0, size]. 如果鏈表中只有一個節點,是可以插入到位置1的,位置0的節點next指向這個節點就可以了。

public void insert(int position, int data) {
        // 創建新節點
        Node newNode = new Node(data);

        // 判斷鏈表是否為空
        if(head == null) {
            // 如果為空,那只允許插入到第一個節點,也就是位置0處,否則報錯
            if (position != 0){
                throw new RuntimeException("鏈表為空");
            } else {
                head = newNode;
            }
        } else { // 至少有一個節點
            // 判斷位置是否符合要求
            if(position < 0 || position > size) {
                throw new RuntimeException("超出范圍");
            } else {
                // 插入頭節點
                if(position == 0) {
                    insertFirst(data);
                } else if (position == size) { // 插入尾節點
                    insertLast(data);
                } else {
                    Node prevNode = head;
                    // 注意是position -1,如果插入到1位置,那head就是前一個節點,無需循環,position-1 -> 0, 不循環
                    for (int i = 0; i < position -1; i++) {
                        prevNode = prevNode.next;
                    }
                    // prevNode就是前一個節點。
                    // nextNode是后一 個節點
                    Node nextNode = prevNode.next;
                    // 新節點的鏈接:前一個節點的next指向新節點,新節點的next指向后一個節點
                    prevNode.next = newNode;
                    newNode.next = nextNode;
                }
            }
        }
     size++; }

  插入功能完成后,就要驗證實現的對不對,簡單的辦法就是把鏈表中的數據打印出來,遍歷整個鏈表。找一個臨時變量curNode, 把head賦值給它, 那么curNode也就指向了第一個節點,此時判斷curNode是不是null, 如果是,鏈表為空,什么都不用做了。如果不是,把curNode的data打印出來,再把curNode.next 賦值給curNode, 這時curNode就指向第二個節點。再判斷curNode是不是null, 如果不是,繼續打印它的data,把curNode.next 賦值給curNode, 這時curNode指向第三個節點。 可以發現,就是不停地判斷curNode是否為null, 如果不是,那就打印data,並curNode.next 賦值給curNode,讓curNode指向下一個節點,直到curNode 為null。這里要注意它和插入尾節點遍歷的不同,插入尾節點,是判斷.next是否為null,這里只判斷節點是否為null

public void display() {
        Node curNode = head;

        while (curNode != null) {
            System.out.print(curNode.data);
            curNode = curNode.next;
        }
    }

  刪除節點,刪除節點也有很多情況,刪除頭節點,刪除某個位置節點,刪除某個指定的節點。刪除頭節點比較簡單,直接讓head指向head.next 就可以了,刪除方法通常會返回要刪除的節點

 

public Node deleteFirst() {
    if (head == null) {
        throw new RuntimeException("鏈表為空");
    }
    Node firstDeleted = head;
    head = head.next;
    return firstDeleted;
}

  刪除某個指定的節點,分為三種情況,這個節點正好是頭節點,這個節點在中間或尾節點,這個節點沒有找到。當刪除某個節點時,不僅要找到這個節點,還要找到這個節點的上一個節點,還是因為鏈表是單向的,刪除某個節點,就要讓某個節點的上一個節點的next指向這個節點的next指向的下一個節點,那遍歷查找某個節點時,還要記錄前一個節點。

public Node deleteKey(int key){
    if (head == null) {
        throw new RuntimeException("鏈表為空");
    }

    Node curNode = head;
    Node prevNode = null;

    // 刪除的某個key正好位於頭節點中
    if(curNode.data == key){
        head = curNode.next;
        return curNode;
    }

    // 刪除的某個key位於中間節點或尾節點
    while (curNode != null && curNode.data != key) {
        prevNode = curNode; //記錄前一個節點
        curNode = curNode.next;
    }

    // 如果找到了
    if(curNode != null){
        prevNode.next = curNode.next;
        return curNode;
    }

    // 如果沒有找 curNode == null
    System.out.println("沒有找到");
    return null;
}

  刪除某個位置的節點,要先找到這個節點,同時記錄上一個節點。怎么才能找到這個節點呢?遍歷到該位置。

public Node deleteByPostion(int postion) {
    if (head == null) {
        throw new RuntimeException("鏈表為空");
    }
    
    if(postion < 0 || postion >= size) {
        throw new RuntimeException("位置不對");
    }
    
    if(postion == 0){
        Node first = head;
        head = head.next;
        return first;
    }

    Node curNode = head;
    Node prevNode = null;
    for (int i = 0; i < postion; i++) {
        prevNode = curNode;
        curNode = curNode.next;
    }
    
    prevNode.next = curNode.next;
    
    return; curNode;
}

  查找

public int find(int data){
    Node curNode = head;
    int index = 0;
    boolean isFind = false;

    while (curNode != null){
        if(curNode.data == data){
            isFind = true;
            break;
        } else {
            index++;
            curNode = curNode.next;
        }
    }

    if(isFind) {
        return index;
    } else {
        return -1;
    }
}

  

 


免責聲明!

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



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