Java基礎系列22:有關鏈表的經典面試題目解析與代碼實現

前言:在上一篇的java基礎系列文章(PS:https://www.zifangsky.cn/933.html)中,我介紹了什麼是鏈表、鏈表的優缺點以及常見鏈表結構(如:單向鏈表、雙向鏈表、循環鏈表)的定義、遍歷、插入、刪除等基本操做的代碼實現。所以,在這一篇文章中我將進一步介紹如何解析有關鏈表的常見問題,最後則是給出其Java實現的參考代碼javascript

問題1:找到鏈表的倒數第n個節點

(1)思路分析:

這個問題很簡單,咱們可使用多種方法來找到鏈表的倒數第n個節點。其中幾種最多見的實現思路以下:html

i)使用蠻力法:從鏈表的第一個節點開始向後移動,針對每個節點都統計它後面還有多少個節點。若是節點數小於n-1個,則表示整個鏈表長度不足n個,返回提示信息「鏈表中總節點數目不足n個」;若是節點數大於n-1個,則向後移動一個節點,並繼續統計該節點後面的節點個數;若是節點數恰好爲n-1個,則表示已經找到目標節點,算法結束java

時間複雜度:O(n²)。由於針對每一個節點都須要掃描一次它以後的全部節點。從時間複雜度能夠看出,這種算法是一種效率不好的算法,所以不考慮使用node

ii)使用散列表(哈希表):遍歷一次鏈表,在遍歷每一個節點的時候分別在散列表中存儲<節點位置,節點地址>。假設鏈表長度爲M,則目標問題轉換成爲:求鏈表中正數第M-n+1個節點,也就是返回散列表中主鍵爲M-n+1的值便可面試

時間複雜度:O(n)。由於須要遍歷一次鏈表算法

iii)不使用散列表求解問題。思路相似上面的散列表實現思路,不一樣的是此次是經過兩次遍歷鏈表的操做來實現:第一次遍歷鏈表獲得鏈表的長度M,第二次遍歷找到正數第M-n+1個節點數據結構

時間複雜度:O(n)。時間開銷表如今第一次遍歷確認鏈表長度的時間開銷以及從表頭開始尋找第M-n+1個節點的時間開銷,即:T(n) = O(n) + O(m) ≈ O(n)oop

iv)掃描一次鏈表就解決問題。定義兩個指針都指向鏈表的表頭節點,其中一個節點先移動 n-1 次,而後兩個指針同時開始向後移動,當處於前面的指針移動到表尾節點時,則此時處於後面的指針便是咱們須要求取的節點測試

時間複雜度:O(n)spa

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import java.util.HashMap;
import java.util.Map;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;

/** * 找到鏈表的倒數第n個節點 * @author zifangsky * */
public class Question1 {

    /** * 方法一:使用散列表。經過遍歷將全部節點存儲到散列表中,再從散列表中返回目標節點 * * @時間複雜度 O(n) * @param headNode * @param n * @return */
    public SinglyNode Method1(SinglyNode headNode,int n){
        Map<Integer,SinglyNode> nodeMap = new HashMap<>();
        SinglyNode currentNode = headNode;

        for(int i=1;currentNode!=null;i++){
            nodeMap.put(i, currentNode);
            currentNode = currentNode.getNext();
        }

        if(n < 1 || n > nodeMap.size()){
            throw new RuntimeException("輸入參數存在錯誤");
        }else{
            return nodeMap.get(nodeMap.size() - n + 1);
        }
    }

    /** * 方法二:首先遍歷獲取鏈表長度,再次遍歷獲得目標節點 * * @時間複雜度 T(n)=O(n)+O(n)≈O(n) * @param headNode * @param n * @return */
    public SinglyNode Method2(SinglyNode headNode,int n){
        //1,獲取鏈表長度
        int length = 0;
        SinglyNode currentNode = headNode;

        while(currentNode != null){
            length++;
            currentNode = currentNode.getNext();
        }

        if(n < 1 || n > length){
            throw new RuntimeException("輸入參數存在錯誤");
        }else{//2,再次遍歷獲得目標節點
            currentNode = headNode;
            for(int i=1;i<length-n+1;i++){
                currentNode = currentNode.getNext();
            }
            return currentNode;
        }
    }

    /** * 方法三:定義兩個指針,兩者相差n-1個節點,而後一塊兒移動直到鏈表末尾 * * @時間複雜度 O(n) * @param headNode * @param n * @return */
    public SinglyNode Method3(SinglyNode headNode,int n){
        SinglyNode frontNode=headNode,laterNode=headNode;

        //1,frontNode先移動 n-1 次
        for(int i=1;i<n;i++){
            if(frontNode != null){
                frontNode = frontNode.getNext();
            }else{
                return null;
            }
        }

        //2,frontNode和laterNode一塊兒移動到鏈表結束
        while(frontNode != null && frontNode.getNext() != null){
            frontNode = frontNode.getNext();
            laterNode = laterNode.getNext();
        }
        return laterNode;
    }


    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(11);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=5;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //方法一
        System.out.println("方法一:" + Method1(headNode, 2));
        //方法二
        System.out.println("方法二:" + Method2(headNode, 2));
        //方法三
        System.out.println("方法三:" + Method3(headNode, 2));
    }

}複製代碼

輸出以下:

方法一:SinglyNode [data=44]
方法二:SinglyNode [data=44]
方法三:SinglyNode [data=44]複製代碼

問題2:如何判斷給定的鏈表是以NULL結尾,仍是造成了一個環?

(1)思路分析:

i)蠻力法:從表頭開始遍歷,針對每一個節點均檢查是否存在它以後的某個節點的後繼指針指向該節點,若是存在則說明該鏈表存在環。若是一直遍歷到表尾節點都未發現這種節點,則說明該鏈表不存在環。很顯然這種算法是一種效率不好的算法,所以不考慮使用

ii)使用散列表(哈希表):從表頭開始逐個遍歷鏈表中的每一個節點,並檢查其是否已經存在散列表中。若是存在則說明已經訪問過該節點了,也就是存在環;若是一直到表尾都沒有出現已經訪問過的節點,則說明該鏈表不存在環

時間複雜度:O(n)

iii)Floyd環斷定算法:使用兩個在鏈表中具備不一樣移動速度的指針(如:fastNode每次移動兩個節點,slowNode每次移動一個節點),兩個指針同時從表頭開始移動,若是在某一時刻它們相遇了,則代表該鏈表存在環。緣由很簡單:快速移動指針和慢速移動指針將會指向同一位置的惟一可能狀況就是整個或者部分鏈表是一個環

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import java.util.HashMap;
import java.util.Map;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;

/** * 判斷給定的鏈表是以NULL結尾,仍是造成了一個環? * @author zifangsky * */
public class Question2 {

    /** * 方法一:使用散列表。從表頭開始逐個遍歷鏈表中的每一個節點,檢查其是否已經存在散列表中。 * 若是存在則說明已經訪問過該節點了,也就是存在環;若是一直到表尾都沒有出現已經訪問過的節點, * 則說明該鏈表不存在環 * * @時間複雜度 O(n) * @param headNode * @return */
    public boolean Method1(SinglyNode headNode){
        Map<Integer,SinglyNode> nodeMap = new HashMap<>();
        SinglyNode currentNode = headNode;

        for(int i=1;currentNode!=null;i++){
            if(nodeMap.containsValue(currentNode)){
                return true;
            }else{
                nodeMap.put(i, currentNode);
                currentNode = currentNode.getNext();
            }
        }

        return false;
    }

    /** * Floyd環斷定算法: * 使用兩個在鏈表中具備不一樣移動速度的指針同時移動,一旦它們進入環就必定會相遇 * 緣由:fast指針和slow指針只有當整個或者部分鏈表是一個環時纔會相遇 * * @時間複雜度 O(n) * @param headNode * @return */
    public boolean Method2(SinglyNode headNode){
        SinglyNode fastNode=headNode,slowNode=headNode;

        while(slowNode.getNext() != null && fastNode.getNext() != null && fastNode.getNext().getNext() != null){
            slowNode = slowNode.getNext();
            fastNode = fastNode.getNext().getNext();

            if(slowNode == fastNode){
                return true;
            }
        }
        return false;
    }

    @Test
    public void testMethods(){
        SinglyNode headNode1 = new SinglyNode(11);

        SinglyNode currentNode = headNode1;
        for(int i=2;i<=5;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //鏈表headNode2,人爲構造了一個環
        SinglyNode headNode2 = new SinglyNode(11);
        SinglyNode ringStartNode = null;
        currentNode = headNode2;
        for(int i=2;i<=8;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
            if(i == 3){
                ringStartNode = tmpNode;
            }else if(i == 8){
                tmpNode.setNext(ringStartNode);
            }
        }

        //方法一
        System.out.println("方法一:鏈表headNode1是否存在環:" + Method1(headNode1)
            + ";鏈表headNode2是否存在環:" + Method1(headNode2));
        //方法二
        System.out.println("方法二:鏈表headNode1是否存在環:" + Method2(headNode1)
        + ";鏈表headNode2是否存在環:" + Method2(headNode2));
    }

}複製代碼

輸出以下:

方法一:鏈表headNode1是否存在環:false;鏈表headNode2是否存在環:true
方法二:鏈表headNode1是否存在環:false;鏈表headNode2是否存在環:true複製代碼

問題3:如何判斷給定的鏈表是以NULL結尾,仍是造成了一個環?若是鏈表中存在環,則找到環的起始節點

(1)思路分析:

首先使用Floyd環斷定算法判斷一個鏈表是否存在環。在找到環以後,將slowNode從新設置爲表頭節點,接下來slowNode和fastNode每次分別移動一個節點,當它們再次相遇時即爲環的起始節點

時間複雜度:O(n)

證實:

設飛環長度爲:C1,整個環的長度爲:C2,兩個指針相遇時走過的環中的弧長爲:C3
第一次相遇時:

Sslow = C1 + C3

Sfast = C1 + C2 + C3

且:Sfast = 2Sslow

則:C1 = C2 – C3

當slowNode重置爲表頭節點,兩個指針只須要分別移動C1便可第二次相遇:

slowNode移動長度:C1,此時slowNode的位置是環的開始節點

fastNode移動長度:C1 = C2 – C3,也就是說fastNode此時的位置是:初始位置C3 + C2 – C3 = C2,也就是說fastNode此時恰好移動到環的開始節點,兩者相遇

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;

/** * 判斷給定的鏈表是以NULL結尾,仍是造成了一個環?若是鏈表中存在環,則找到環的起始節點 * @author zifangsky * */
public class Question3 {

    /** * 在找到環以後,將slowNode從新設置爲表頭節點,接下來slowNode和fastNode每次分別移動一個節點, * 當它們再次相遇時即爲環的起始節點 * * @時間複雜度 O(n) * @param headNode * @return */
    public SinglyNode findLoopStartNode(SinglyNode headNode){
        SinglyNode fastNode=headNode,slowNode=headNode;
        boolean loopExists = false; //是否存在環的標識

        while(slowNode.getNext() != null && fastNode.getNext() != null && fastNode.getNext().getNext() != null){
            slowNode = slowNode.getNext();
            fastNode = fastNode.getNext().getNext();

            if(slowNode == fastNode){
                loopExists = true;
                break;
            }
        }

        //若是存在環,則slowNode回到表頭
        if(loopExists){
            slowNode = headNode;
            while(slowNode != fastNode){
                slowNode = slowNode.getNext();
                fastNode = fastNode.getNext();
            }
            return fastNode;

        }
        return null;
    }

    @Test
    public void testMethods(){
        SinglyNode headNode1 = new SinglyNode(11);

        SinglyNode currentNode = headNode1;
        for(int i=2;i<=5;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //鏈表headNode2,人爲構造了一個環
        SinglyNode headNode2 = new SinglyNode(11);
        SinglyNode ringStartNode = null;
        currentNode = headNode2;
        for(int i=2;i<=8;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
            if(i == 3){
                ringStartNode = tmpNode;
            }else if(i == 8){
                tmpNode.setNext(ringStartNode);
            }
        }

        System.out.println("鏈表headNode1的環的起始節點:" + findLoopStartNode(headNode1)
            + ";鏈表headNode2的環的起始節點:" + findLoopStartNode(headNode2));
    }

}複製代碼

輸出以下:

鏈表headNode1的環的起始節點:null;鏈表headNode2的環的起始節點:SinglyNode [data=33]複製代碼

問題4:如何判斷給定的鏈表是以NULL結尾,仍是造成了一個環?若是鏈表中存在環,則返回環的長度

(1)思路分析:

首先使用Floyd環斷定算法判斷一個鏈表是否存在環。在找到環以後,保持fastNode不動,接下來slowNode每次移動一個節點,同時計數器加一,當它們再次相遇時便可求出環的長度

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;

/** * 判斷給定的鏈表是以NULL結尾,仍是造成了一個環?若是鏈表中存在環,則返回環的長度 * @author zifangsky * */
public class Question4 {
    /** * 在找到環以後,保持fastNode不動,接下來slowNode每次移動一個節點,同時計數器加一, * 當它們再次相遇時便可求出環的長度 * * @時間複雜度 O(n) * @param headNode * @return */
    public int findLoopLength(SinglyNode headNode){
        SinglyNode fastNode=headNode,slowNode=headNode;
        boolean loopExists = false; //是否存在環的標識
        int length = 0; //環的長度

        while(slowNode.getNext() != null && fastNode.getNext() != null && fastNode.getNext().getNext() != null){
            slowNode = slowNode.getNext();
            fastNode = fastNode.getNext().getNext();

            if(slowNode == fastNode){
                loopExists = true;
                break;
            }
        }

        //若是存在環,則保持fastNode不動,slowNode逐個移動,直到兩者再次相遇
        if(loopExists){
            slowNode = slowNode.getNext();
            length++;
            while(slowNode != fastNode){
                slowNode = slowNode.getNext();
                length++;
            }
        }
        return length;
    }

    @Test
    public void testMethods(){
        SinglyNode headNode1 = new SinglyNode(11);

        SinglyNode currentNode = headNode1;
        for(int i=2;i<=5;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //鏈表headNode2,人爲構造了一個環
        SinglyNode headNode2 = new SinglyNode(11);
        SinglyNode ringStartNode = null;
        currentNode = headNode2;
        for(int i=2;i<=8;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
            if(i == 3){
                ringStartNode = tmpNode;
            }else if(i == 8){
                tmpNode.setNext(ringStartNode);
            }
        }

        System.out.println("鏈表headNode1的環的長度:" + findLoopLength(headNode1)
            + ";鏈表headNode2的環的長度:" + findLoopLength(headNode2));
    }
}複製代碼

輸出以下:

鏈表headNode1的環的長度:0;鏈表headNode2的環的長度:6複製代碼

問題5:向有序鏈表中插入一個節點

(1)思路分析:

遍歷鏈表,找到存放元素的正確位置以後,插入節點

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 向有序鏈表中插入一個節點 * @author zifangsky * */
public class Question5 {

    /** * 向有序鏈表中插入一個節點 * * @時間複雜度 O(n) * @param headNode * @param newNode * @return */
    public SinglyNode insertIntoSortedList(SinglyNode headNode,SinglyNode newNode){
        if(newNode.getData() <= headNode.getData()){
            newNode.setNext(headNode);
            return newNode;
        }else{
            SinglyNode currentNode=headNode;
            while(currentNode.getNext() != null && newNode.getData() > currentNode.getNext().getData()){
                currentNode = currentNode.getNext();
            }
            newNode.setNext(currentNode.getNext());
            currentNode.setNext(newNode);

            return headNode;
        }
    }

    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(11);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=5;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //遍歷初始連接
        SinglyNodeOperations.print(headNode);

        SinglyNode newNode = new SinglyNode(66);
        headNode = insertIntoSortedList(headNode, newNode);

        //遍歷最終結果
        SinglyNodeOperations.print(headNode);
    }


}複製代碼

輸出以下:

11 22 33 44 55 
11 22 33 44 55 66複製代碼

問題6:如何逆置單向鏈表?

(1)思路分析:

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 如何逆置單向鏈表? * @author zifangsky * */
public class Question6 {

    /** * @時間複雜度 O(n) * @param headNode * @return */
    public SinglyNode ReverseList(SinglyNode headNode){
        SinglyNode tempNode=null,nextNode=null;

        while(headNode != null){
            nextNode = headNode.getNext();
            headNode.setNext(tempNode);
            tempNode = headNode;
            headNode = nextNode;
        }
        return tempNode;
    }

    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(11);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=5;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //遍歷初始鏈表
        SinglyNodeOperations.print(headNode);

        headNode = ReverseList(headNode);

        //遍歷最終結果
        SinglyNodeOperations.print(headNode);

    }

}複製代碼

輸出以下:

11 22 33 44 55 
55 44 33 22 11複製代碼

問題7:如何逐對逆置單向鏈表?

若是初始節點鏈表是:11 –> 22 –> 33 –> 44 –> 55 –> 66 –> 77,那麼通過逐對逆置後,新鏈表變成:22 –> 11 –> 44 –> 33 –> 66 55 –> 77

(1)思路分析:

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 如何逐對逆置單向鏈表? * @author zifangsky * */
public class Question7 {

    /** * @時間複雜度 O(n) * @param headNode * @return */
    public SinglyNode ReverseList(SinglyNode headNode){
        SinglyNode tempNode=null;

        if(headNode == null || headNode.getNext() == null){
            return headNode;
        }else{
            tempNode = headNode.getNext();
            headNode.setNext(tempNode.getNext());
            tempNode.setNext(headNode);
            tempNode.getNext().setNext(ReverseList(headNode.getNext()));

            return tempNode;
        }
    }

    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(11);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=7;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //遍歷初始鏈表
        SinglyNodeOperations.print(headNode);

        headNode = ReverseList(headNode);

        //遍歷最終結果
        SinglyNodeOperations.print(headNode);

    }

}複製代碼

輸出以下:

11 22 33 44 55 66 77 
22 11 44 33 66 55 77複製代碼

問題8:如何從表尾開始輸出鏈表?

(1)思路分析:

使用遞歸便可實現從表尾開始輸出鏈表

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 如何從表尾開始輸出鏈表? * @author zifangsky * */
public class Question8 {

    /** * 思路:遞歸,從鏈表末尾開始輸出 * @時間複雜度 O(n) * @param headNode * @return */
    public void printFromEnd(SinglyNode headNode){
        if(headNode != null && headNode.getNext() != null){
            printFromEnd(headNode.getNext());
        }
        System.out.print(headNode.getData() + " ");
    }

    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(11);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=5;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //遍歷初始鏈表
        SinglyNodeOperations.print(headNode);

        //從末尾開始遍歷鏈表
        printFromEnd(headNode);
    }

}複製代碼

輸出以下:

11 22 33 44 55 
55 44 33 22 11複製代碼

問題9:判斷一個鏈表的長度是奇數仍是偶數?

(1)思路分析:

定義一個在鏈表中每次移動兩個節點的指針。若是最後指針指向NULL,則說明此鏈表的長度是偶數;若是最後指針指向表尾節點,則說明此鏈表的長度是奇數

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;

/** * 判斷一個鏈表的長度是奇數仍是偶數? * @author zifangsky * */
public class Question9 {

    /** * 思路:定義一個在鏈表中每次移動兩個節點的指針,若是最後指針指向NULL, * 則說明此鏈表的長度是偶數 * @時間複雜度 O(n) * @param headNode * @return */
    public void CheckList(SinglyNode headNode){
        while(headNode != null && headNode.getNext() != null){
            headNode = headNode.getNext().getNext();
        }
        if(headNode == null){
            System.out.println("此鏈表長度爲偶數");
        }else{
            System.out.println("此鏈表長度爲奇數");
        }
    }

    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(11);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=5;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        CheckList(headNode);
    }

}複製代碼

輸出以下:

此鏈表長度爲奇數複製代碼

問題10:如何將兩個有序鏈表合併成一個新的有序鏈表?

(1)思路分析:

使用遞歸依次找出每一個位置上的最小的節點

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 如何將兩個有序鏈表合併成一個新的有序鏈表? * @author zifangsky * */
public class Question10 {

    /** * 思路:遞歸依次比較大小 * @時間複雜度 O(n/2) = O(n) * @param headNode * @return */
    public SinglyNode MergeList(SinglyNode headNode1,SinglyNode headNode2){
        SinglyNode result = null;

        if(headNode1 == null) return headNode2;
        if(headNode2 == null) return headNode1;

        if(headNode1.getData() <= headNode2.getData()){
            result = headNode1;
            result.setNext(MergeList(headNode1.getNext(),headNode2));
        }else{
            result = headNode2;
            result.setNext(MergeList(headNode1, headNode2.getNext()));
        }
        return result;
    }

    @Test
    public void testMethods(){
        SinglyNode a = new SinglyNode(11);
        SinglyNode b = new SinglyNode(22);

        SinglyNode currentA = a,currentB = b;
        for(int i=3;i<=8;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);

            if(i%2 == 0){
                currentB.setNext(tmpNode);
                currentB = tmpNode;
            }else{
                currentA.setNext(tmpNode);
                currentA = tmpNode;
            }

        }

        //遍歷初始鏈表
        System.out.print("A: ");
        SinglyNodeOperations.print(a);
        System.out.print("B: ");
        SinglyNodeOperations.print(b);

        System.out.print("合併以後: ");
        SinglyNodeOperations.print(MergeList(a, b));
    }

}複製代碼

輸出以下:

A: 11 33 55 77 
B: 22 44 66 88 
合併以後: 11 22 33 44 55 66 77 88複製代碼

問題11:如何找到鏈表的中間節點?

(1)思路分析:

i)遍歷兩次:第一次遍歷獲得鏈表的長度N,第二次遍歷定位到 N/2 個節點,即爲中間節點

ii)使用散列表:略

iii)分別定義兩個移動速度爲:1節點/次、2節點/次的指針,當速度較快的指針移動到鏈表末尾時,此時速度較慢的指針指向的節點即爲中間節點

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 如何找到鏈表的中間節點? * @author zifangsky * */
public class Question11 {

    /** * 思路:分別定義兩個移動速度爲:1節點/次、2節點/次的指針, * 當速度較快的指針移動到鏈表末尾時,此時速度較慢的指針指向的節點即爲中間節點 * @時間複雜度 O(n/2) = O(n) * @param headNode * @return */
    public SinglyNode findMiddle(SinglyNode headNode){
        if(headNode != null){
            SinglyNode slow = headNode,fast = headNode;

            while(fast != null && fast.getNext() != null && fast.getNext().getNext() != null){
                slow = slow.getNext();
                fast = fast.getNext().getNext();
            }
            return slow;
        }else{
            return null;
        }
    }

    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(11);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=6;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //遍歷初始連接
        SinglyNodeOperations.print(headNode);

        System.out.println("鏈表中間節點是: " + findMiddle(headNode));
    }

}複製代碼

輸出以下:

11 22 33 44 55 66 
鏈表中間節點是: SinglyNode [data=33]複製代碼

問題12:假設兩個單向鏈表在某個節點相交後,成爲一個單向鏈表,求該交點?

(1)思路分析:

i)使用蠻力法:遍歷第一個鏈表,將第一個鏈表中的每一個節點都和第二個鏈表中的每一個節點比較,若是出現相等的節點時,即爲相交節點

時間複雜度:O(mn)

ii)使用散列表:遍歷第一個鏈表,將第一個鏈表中的每一個節點都存入散列表中。再次遍歷第二個鏈表,對於第二個鏈表中的每一個節點均檢查是否已經存在於散列表中,若是存在則說明該節點爲交點

時間複雜度:O(m) + O(n) = O(max(m,n))

iii)使用棧求解:定義兩個棧分別存儲兩個鏈表。分別遍歷兩個鏈表並壓入到對應的棧中。比較兩個棧的棧頂元素,若是兩者相等則將該棧頂元素存儲到臨時變量中,並彈出兩個棧的棧頂元素。重複上述操做一直到兩個棧的棧頂元素不相等爲止。最後存儲的的臨時變量即爲交點

時間複雜度:O(m) + O(n) = O(max(m,n))

iv)分別得到兩個鏈表的長度,計算出兩個鏈表的長度差d,較長的鏈表首先移動d步,而後兩個鏈表同時向表尾移動,當出現兩個節點相同時即爲交點

時間複雜度:O(m) + O(n) = O(max(m,n))

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import java.util.HashMap;
import java.util.Map;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 找出兩個相交鏈表的交點 * @author zifangsky * */
public class Question12 {

    /** * 思路:使用散列表求解 * @時間複雜度 O(m) + O(n) = O(max(m,n)),即:O(m)或O(n) * @param headNode * @return */
    public SinglyNode findIntersection1(SinglyNode headNode1,SinglyNode headNode2){    
        Map<Integer,SinglyNode> nodeMap = new HashMap<>();

        for(int i=1;headNode1!=null;i++){
            nodeMap.put(i, headNode1);
            headNode1 = headNode1.getNext();
        }

        while (headNode2 != null) {
            if(nodeMap.containsValue(headNode2)){
                return headNode2;
            }else{
                headNode2 = headNode2.getNext();
            }
        }
        return null;
    }

    /** * 思路:1,分別得到兩個鏈表的長度;2,計算出兩個鏈表的長度差d; * 3,較長的鏈表首先移動d步,而後兩個鏈表同時向表尾移動 * 4,當出現兩個節點相同時即爲交點 * @時間複雜度 O(m) + O(n) + O(1) + O(d) + O(min(m,n)) = O(max(m,n)) * @param headNode1 * @param headNode2 * @return */
    public SinglyNode findIntersection2(SinglyNode headNode1,SinglyNode headNode2){
        int length1 = 0,length2 = 0; //兩個鏈表節點數
        int diff = 0;
        SinglyNode temp1 = headNode1,temp2 = headNode2;
        //1
        while (temp1 != null) {
            length1++;
            temp1 = temp1.getNext();
        }

        while (temp2 != null) {
            length2++;
            temp2 = temp2.getNext();
        }
        //二、3
        if(length1 > 0 && length2 > 0 && length2 >= length1){
            diff = length2 - length1;

            for(int i=1;i<=diff;i++){
                headNode2 = headNode2.getNext();
            }
        }else if(length1 > 0 && length2 > 0 && length2 < length1){
            diff = length1 - length2;

            for(int i=1;i<=diff;i++){
                headNode1 = headNode1.getNext();
            }
        }else{
            return null;
        }
        //4
        while(headNode1 != null && headNode2 != null){
            if(headNode1 == headNode2){
                return headNode1;
            }else{
                headNode1 = headNode1.getNext();
                headNode2 = headNode2.getNext();
            }
        }
        return null;    
    }

    @Test
    public void testMethods(){
        //人爲構造兩個相交的鏈表 
        SinglyNode a = new SinglyNode(11);
        SinglyNode b = new SinglyNode(22);

        SinglyNode currentA = a,currentB = b;
        for(int i=3;i<=8;i++){
            SinglyNode tmpNode = new SinglyNode(11 * i);

            if(i < 7){
                if(i%2 == 0){
                    currentB.setNext(tmpNode);
                    currentB = tmpNode;

                    SinglyNode tmpNode2 = new SinglyNode(11 * i + 1);
                    currentB.setNext(tmpNode2);
                    currentB = tmpNode2;
                }else{
                    currentA.setNext(tmpNode);
                    currentA = tmpNode;
                }
            }else{
                currentB.setNext(tmpNode);
                currentB = tmpNode;
                currentA.setNext(tmpNode);
                currentA = tmpNode;
            }

        }

        //遍歷初始鏈表
        System.out.print("A: ");
        SinglyNodeOperations.print(a);
        System.out.print("B: ");
        SinglyNodeOperations.print(b);

        System.out.println("方法一,其交點是: " + findIntersection1(a,b));
        System.out.println("方法二,其交點是: " + findIntersection2(a,b));
    }

}複製代碼

輸出以下:

A: 11 33 55 77 88 
B: 22 44 45 66 67 77 88 
方法一,其交點是: SinglyNode [data=77]
方法二,其交點是: SinglyNode [data=77]複製代碼

問題13:如何把一個循環鏈表分割成兩個長度相等的部分?若是鏈表的節點數是奇數,那麼讓第一個鏈表的節點數比第二個鏈表多一個

(1)思路分析:

定義兩個移動速度不同的指針:fastNode每次移動兩個節點,slowNode每次移動一個節點。當slowNode移動到中間節點的時候,若是鏈表有奇數個節點,此時fastNode.getNext()指向headNode;若是鏈表有偶數個節點,此時fastNode.getNext().getNext()指向headNode

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.CircularNode;
import cn.zifangsky.linkedlist.CircularNodeOperations;

/** * 如何把一個循環鏈表分割成兩個長度相等的部分? * @author zifangsky * */
public class Question13 {

    /** * 思路: * 定義兩個移動速度不同的指針:fastNode每次移動兩個節點,slowNode每次移動一個節點。 * 當slowNode移動到中間節點的時候,若是鏈表有奇數個節點,此時fastNode.getNext()指向headNode; * 若是鏈表有偶數個節點,此時fastNode.getNext().getNext()指向headNode * @時間複雜度 O(n) * @param headNode */
    public void splitList(CircularNode headNode){
        if(headNode == null)
            return;
        CircularNode fastNode = headNode,slowNode = headNode;

        while(fastNode.getNext() != headNode && fastNode.getNext().getNext() != headNode){
            fastNode = fastNode.getNext().getNext();
            slowNode = slowNode.getNext();
        }

        CircularNode result1 = null,result2 = null; //定義兩個分割以後的子鏈表
        result1 = headNode; //設置前半部分的head指針
        if(headNode.getNext() != headNode){
            result2 = slowNode.getNext(); //設置後半部分的head指針
        }

        //若是鏈表有偶數個節點,此時fastNode再向後移動一個節點
        if(fastNode.getNext().getNext() == headNode){
            fastNode = fastNode.getNext();
        }
        fastNode.setNext(slowNode.getNext()); //把後半部分閉合成環
        slowNode.setNext(headNode); //把前半部分閉合成環

        //測試輸出兩個子鏈表
        System.out.print("子鏈表1:");
        CircularNodeOperations.print(result1);

        System.out.print("子鏈表2:");
        if(result2 != null){
            CircularNodeOperations.print(result2);
        }
    }



    @Test
    public void testMethods(){
        CircularNode headNode = new CircularNode(11);

        CircularNode currentNode = headNode;
        for(int i=2;i<=7;i++){
            CircularNode tmpNode = new CircularNode(11 * i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;

            if(i == 7){
                currentNode.setNext(headNode);
            }
        }

        //遍歷初始連接
        CircularNodeOperations.print(headNode);

        splitList(headNode);
    }

}複製代碼

輸出以下:

11 22 33 44 55 66 77 
子鏈表111 22 33 44 
子鏈表255 66 77複製代碼

問題14:約瑟夫環:N我的想選出一個領頭人。他們排成一個環,沿着環每次數到第M我的就從環中排出該人,並從下一我的開始繼續數。請找出最後留在環中的人

(1)思路分析:

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.CircularNode;

/** * 約瑟夫環問題 * @author zifangsky * */
public class Question14 {

    /** * * @param N 人數 * @param M 每次數數個數 */
    public void getLastPerson(int N,int M){
        //構建一個環
        CircularNode headNode = new CircularNode(1);

        CircularNode currentNode = headNode;
        for(int i=2;i<=N;i++){
            CircularNode tmpNode = new CircularNode(i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;

            if(i == N){
                currentNode.setNext(headNode);
            }
        }

        //當鏈表大於一個節點時一直循環排除下去
        while(headNode.getNext() != headNode){
            for(int i=1;i<M;i++){
                headNode = headNode.getNext();
            }
            headNode.setNext(headNode.getNext().getNext()); //排除headNode.getNext()這我的
        }
        System.out.println("剩下的人是: " + headNode.getData());
    }

    @Test
    public void testMethods(){
        getLastPerson(5,3);
    }

}複製代碼

輸出以下:

剩下的人是: 5複製代碼

問題15:給定一個單向鏈表,要求從鏈表表頭開始找到最後一個知足 i%k==0 的節點

例如:n爲7,k爲3,那麼應該返回第6個節點

(1)思路分析:

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 尋找模節點,即:從鏈表表頭開始找到最後一個知足 i%k==0 的節點 * @author zifangsky * */
public class Question15 {

    /** * 思路:略 * @時間複雜度 O(n) * @param headNode * @param k * @return */
    public SinglyNode getModularNode(SinglyNode headNode,int k){
        SinglyNode result = null;
        if(k <= 0){
            return null;
        }

        for(int i=1;headNode!=null;i++){
            if(i % k == 0){
                result = headNode;
            }
            headNode = headNode.getNext();    
        }
        return result;
    }

    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(1);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=7;i++){
            SinglyNode tmpNode = new SinglyNode(i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //遍歷初始連接
        SinglyNodeOperations.print(headNode);

        System.out.println("目標節點是: " + getModularNode(headNode,3));
    }

}複製代碼

輸出以下:

1 2 3 4 5 6 7 
目標節點是: SinglyNode [data=6]複製代碼

問題16:給定一個單向鏈表,要求從鏈表表尾開始找到第一個知足 i%k==0 的節點

例如:n爲7,k爲3,那麼應該返回第5個節點

(1)思路分析:

時間複雜度:O(n)

(2)示例代碼:

package cn.zifangsky.linkedlist.questions;

import org.junit.Test;

import cn.zifangsky.linkedlist.SinglyNode;
import cn.zifangsky.linkedlist.SinglyNodeOperations;

/** * 從表尾開始尋找模節點,即:從鏈表表尾開始找到第一個知足 i%k==0 的節點 * @author zifangsky * */
public class Question16 {

    /** * 思路: * headNode首先移動k個節點,而後headNode和result再分別逐步向表尾移動, * 當headNode移動到NULL時,此時result即爲目標節點 * @時間複雜度 O(n) * @param headNode * @param k * @return */
    public SinglyNode getModularNode(SinglyNode headNode,int k){
        SinglyNode result = headNode;
        if(k <= 0){
            return null;
        }

        for(int i=1;i<=k;i++){
            if(headNode != null){
                headNode = headNode.getNext();
            }else{
                return null;
            }
        }

        while (headNode != null) {
            result = result.getNext();
            headNode = headNode.getNext();
        }

        return result;
    }

    @Test
    public void testMethods(){
        SinglyNode headNode = new SinglyNode(1);

        SinglyNode currentNode = headNode;
        for(int i=2;i<=7;i++){
            SinglyNode tmpNode = new SinglyNode(i);
            currentNode.setNext(tmpNode);
            currentNode = tmpNode;
        }

        //遍歷初始連接
        SinglyNodeOperations.print(headNode);

        System.out.println("目標節點是: " + getModularNode(headNode,3));
    }

}複製代碼

輸出以下:

1 2 3 4 5 6 7 
目標節點是: SinglyNode [data=5]複製代碼

好了,有關鏈表的經典面試題目解析到這裏就結束了。但願我這裏的拋磚引玉能夠對正在複習這一塊內容的同窗有所幫助,同時也但願能夠進一步加深你們對鏈表的理解,觸類旁通,可以更靈活地使用鏈表這種數據結構

相關文章
相關標籤/搜索