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

原創不易,如需轉載,請註明出處http://www.javashuo.com/article/p-szxcuqsm-dk.html,不然將追究法律責任!!! html

1、鏈表介紹

一、什麼是鏈表?

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

二、鏈表的優缺點?

  • 優勢:
    • 插入和刪除時不需移動其餘元素, 只需改變指針,效率高。
    • 鏈表各個節點在內存中空間不要求連續,空間利用率高。
    • 大小沒有固定,拓展很靈活。
  • 缺點:
    • 查找數據時效率低,由於不具備隨機訪問性。

三、鏈表的種類?

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

2、單鏈的實現和相關操做

單鏈01.png

一、鏈表類的建立(如下均已單鏈表爲基準)

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;
        }
    } 

    //下面就能夠自定義各類鏈表操做。。。
}

二、鏈表添加結點

//找到鏈表的末尾結點,把新添加的數據做爲末尾結點的後續結點
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);
}

三、鏈表刪除節點

//把要刪除結點的前結點指向要刪除結點的後結點,即直接跳過待刪除結點
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;
}

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

//獲取鏈表長度
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;
    }
}

五、查找單鏈表中的倒數第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;
}

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

//也是設置兩個指針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;
}

七、從尾到頭打印單鏈表

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

//方法3、把鏈表中的元素放入棧中再輸出,須要維護額外的棧空間
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+" ");  //出棧操做
        }
    }
}

八、單鏈表的反轉(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;
}

九、判斷鏈表是否有環

鏈表環.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;
}

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

單鏈環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;
}

十一、判斷兩個鏈表是否相交

//兩個鏈表相交,則它們的尾結點必定相同,比較兩個鏈表的尾結點是否相同便可
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;
}

十二、若是鏈表相交,求鏈表相交的起始點

鏈表相交01.png

/**
 * 若是鏈表相交,求鏈表相交的起始點:
 * 一、首先判斷鏈表是否相交,若是兩個鏈表不相交,則求相交起點沒有意義
 * 二、求出兩個鏈表長度之差:len=length1-length2
 * 三、讓較長的鏈表先走len步
 * 四、而後兩個鏈表同步向前移動,每移動一次就比較它們的結點是否相等,第一個相等的結點即爲它們的第一個相交點
 */
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;
    }
}

1三、合併兩個有序的單鏈表(將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;
    }

到此單鏈表的一些常見操做展現的差很少了,若有興趣可繼續深刻研究~~~前端

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

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

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

雙向鏈表.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);
    }
}

二、循環單鏈表、循環雙鏈表(操做和單鏈、雙鏈是同樣的,不贅述了)

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

4、總結

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

我的博客地址:node

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

相關文章
相關標籤/搜索