單鏈表之約瑟夫問題
文章目錄
一、問題描述
約瑟夫( Josephu ) 問題是一個非常著名的有趣的題目。問題具體描述如下:
設編號分別為1、2、3… n 的 n 個人圍坐一圈,約定編號為 k(1≤k≤n)的人從 1 開始報數,數到 m 的那個人出列。出列的人的下一位又從 1 開始報數,數到 m 的那個人繼續出列。以此類推,直到所有人都出列為止,由此產生一個出隊編號的序列,這個序列也就是約瑟夫問題的解。
下面將用一個動圖來描述一下這個問題:
假設有 4 個人圍坐一圈,約定編號為 1 的人開始報數,數到 3 的那個出列。最後產生的出隊編號的序列將會是:3、2、4、1。
二、解決思路
首先要確定解決問題的核心思想:使用一個不帶頭結點的迴圈(環形)連結串列來處理該問題。
假設每個結點代表一個人,那麼一個由 n 個結點組成的迴圈連結串列就相當於是 n 個人圍成的一個圈。那麼約瑟夫問題以環形連結串列的形式來描述就是如下情景:
首先使用 n 個結點構成一個單向迴圈連結串列,然後由第 k 個結點起從 1 開始計數,當計到 m 時,從連結串列中刪除對應結點;接著從被刪除結點的下一個結點開始從 1 計數,當計到 m 時,繼續從連結串列中刪除。依次迴圈往復,直到連結串列中的所有結點都被刪除為止。
那麼對於這個單向迴圈連結串列形式下的約瑟夫問題,我們如何解決呢?
我們可以引入一個輔助指標 helperNode
因為我們的目的是要刪除當前計數為 m 的結點,但是受限於單向連結串列的特性(如果要刪除單鏈表的某個結點,必須要知道該結點的前一個結點),我們無法讓結點自己刪除自己。鑑於這個特性,我們必須要引入一個輔助指標來記錄當前正在計數的結點的前一個結點,這樣才能符合刪除條件的結點從連結串列中刪除。
引入這個輔助指標之後,具體的操作思路如下:
- 每一輪計數開始時,總讓輔助指標
helperNode
初始指向本輪第一個計數的結點; - 從第一個計數的結點開始計數至 m,實際上是向後移動了 m-1 個結點。由於輔助指標總是指向待刪除結點的前一個結點,因此讓需要讓輔助指標從第一個計數結點後移 m-2 個結點
- 輔助指標移動到待刪除結點的前一個結點之後,只需要讓輔助指標指向待刪除的結點的下一個結點即可完成刪除操作;
- 依次迴圈往復,直至只剩最後一個結點;
- 對於環形連結串列判斷是否只有最後一個結點,只需要判斷輔助指標指向的結點是否是輔助指標指向的結點的下一個結點即可。
上面的思路可以用下面一個動圖來描述:
三、程式碼實現
此處演示程式碼使用的結點類是 HeroNode
:
public class HeroNode {
private int no; // 本節點資料
private String name;
private String nickName;
public HeroNode next; // 指向下一個節點
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
// getter and setter and toString
}
約瑟夫問題求解過程如下:
/**
* @Description 單向環形連結串列解決約瑟夫問題
* @Author Ronz
*/
public class No7_Josepfu {
public static void main(String[] args) {
// 構造測試資料
HeroNode node_1 = new HeroNode(1, "宋江", "及時雨");
HeroNode node_2 = new HeroNode(2, "盧俊義", "玉麒麟");
HeroNode node_3 = new HeroNode(3, "吳用", "智多星");
HeroNode node_4 = new HeroNode(4, "公孫勝", "入雲龍");
System.out.println("===============向環形連結串列中插入結點==================");
HeroNode first = insertCircleList(null, node_1);
first = insertCircleList(first, node_2);
first = insertCircleList(first, node_3);
first = insertCircleList(first, node_4);
showList(first);
System.out.println("===============約瑟夫遊戲開始===============");
// 從第 1 個結點開始計數,每次計 3 個數。
josepfuGame(first,1,3);
}
/**
* @Description 1. 約瑟夫遊戲開始
* @Param [first, k, m] 從第 k 個人開始數,每次數 m 個
*/
public static void josepfuGame(HeroNode first,int k, int m){
if (first == null){
System.out.println("連結串列為空!");
return;
}
HeroNode helperNode = first;
// 首先要移動到第 k 個結點,此時輔助指標初始指向第一個計數的結點
for (int i = 1; i < k; i++){
helperNode = helperNode.next;
}
while (helperNode.next.getNo() != helperNode.getNo()){
// 報數, m 個數也就是相當於向後移動 m-1 次,也就是要把第 m-1 個結點去掉
// 由於單鏈表的特點,要去掉第 m-1 個結點,肯定是要讓指標前一個結點,即第(m-2)個結點
for (int j=0; j<m-2; j++){ // 讓指標後移 m-2 個結點
helperNode = helperNode.next;
}
System.out.println(helperNode.next + "退出連結串列了!");
// 刪除結點
helperNode.next = helperNode.next.next;
// 因為下一輪要從剛剛去掉的結點的後面一個結點開始計數了,所以需要讓輔助指標初始指向下一輪第一個計數的結點
helperNode = helperNode.next;
}
System.out.println(helperNode + "退出連結串列了!");
}
/**
* @Description 2. 插入結點到環形連結串列中,用於構造環形連結串列
*/
public static HeroNode insertCircleList(HeroNode first, HeroNode node){
// 判斷連結串列是不是為空,如果為空,就直接插入
if (first == null){
first = node;
// 因為要環形連結串列,而且只有一個結點,所以要我指向我自己
first.next = node;
}else{
// 如果環形連結串列不為空
HeroNode tempNode = first;
while (true){
// 如果到了環形連結串列的最後一個元素
if (tempNode.next.getNo() == first.getNo()){
tempNode.next = node;
// 因為是環形連結串列,所以最後一個結點還要指向第一個結點
node.next = first;
break;
}
tempNode = tempNode.next;
}
}
return first;
}
/**
* @Description 3. 列印單向環形連結串列
*/
public static void showList(HeroNode first){
if (first == null){
System.out.println("連結串列為空!");
return;
}
HeroNode tempNode = first;
while (true){
if (tempNode.next.getNo() == first.getNo()){
System.out.println(tempNode);
break;
}
System.out.println(tempNode);
tempNode = tempNode.next;
}
}
}
執行結果如下:
至此,使用單向環形連結串列成功解決約瑟夫問題。