前言:在上一篇的java基礎系列文章(PS:https://www.zifangsky.cn/933.html)中,我介紹了什麼是連結串列、連結串列的優缺點以及常見連結串列結構(如:單向連結串列、雙向連結串列、迴圈連結串列)的定義、遍歷、插入、刪除等基本操作的程式碼實現。因此,在這一篇文章中我將進一步介紹如何解析有關連結串列的常見問題,最後則是給出其Java實現的參考程式碼
問題1:找到連結串列的倒數第n個節點
(1)思路分析:
這個問題很簡單,我們可以使用多種方法來找到連結串列的倒數第n個節點。其中幾種最常見的實現思路如下:
i)使用蠻力法:從連結串列的第一個節點開始向後移動,針對每一個節點都統計它後面還有多少個節點。如果節點數小於n-1個,則表示整個連結串列長度不足n個,返回提示資訊“連結串列中總節點數目不足n個”;如果節點數大於n-1個,則向後移動一個節點,並繼續統計該節點後面的節點個數;如果節點數剛好為n-1個,則表示已經找到目標節點,演算法結束
時間複雜度:O(n²)。因為針對每個節點都需要掃描一次它之後的所有節點。從時間複雜度可以看出,這種演算法是一種效率很差的演算法,因此不考慮使用
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)
iv)掃描一次連結串列就解決問題。定義兩個指標都指向連結串列的表頭節點,其中一個節點先移動 n-1 次,然後兩個指標同時開始向後移動,當處於前面的指標移動到表尾節點時,則此時處於後面的指標即是我們需要求取的節點
時間複雜度: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;
/**
* 找到連結串列的倒數第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();
}
//2、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
子連結串列1:11 22 33 44
子連結串列2:55 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]複製程式碼
好了,有關連結串列的經典面試題目解析到這裡就結束了。希望我這裡的拋磚引玉可以對正在複習這一塊內容的同學有所幫助,同時也希望可以進一步加深大家對連結串列的理解,舉一反三,能夠更靈活地使用連結串列這種資料結構