本篇文章介紹數據結構中的環形鏈表。
介紹
環形鏈表,類似於單鏈表,也是一種鏈式存儲結構,環形鏈表由單鏈表演化過來。單鏈表的最后一個結點的鏈域指向NULL,而環形鏈表的建立,不要專門的頭結點,讓最后一個結點的鏈域指向鏈表結點。 簡單點說鏈表首位相連,組成環狀數據結構。如下圖結構:
而在環形鏈表中,最為著名的即是約瑟夫環問題。
約瑟夫環問題
問題介紹:
設編號為1、2、3、... 、n的n個人圍坐一圈,約定編號為k(1<=k<=n)的人從1開始報數,數到m的那個人出列,它的下一位又從1開始報數,數到m的那個人又出列。依次類推,直到所有人出列為止,由此產生一個出隊編號的序列。
我們可以舉個例子來分析一下:
假設一共有5個人,即n = 5;從第一個人開始報數,即k = 1;數到2的人出列,即m = 2。
示意圖如下:
出隊列的順序即為:2 -> 4 -> 1 -> 5 -> 3
那么我們首先得構建出一個單向的環形鏈表。
實現分析:
- 先創建第一個節點,讓first指向該節點,並形成環狀
- 每創建一個新的節點就將該節點加入到已有的環形鏈表中
分析完畢,我們用代碼實現一下:
//創建一個環形的單向鏈表
class CircleSingleLinkedList {
// 創建一個first節點,當前沒有編號
private Boy first = null;
// 添加節點,構建成一個環形鏈表
public void addBoy(int nums) {
// 對nums做一個校驗
if (nums < 1) {
System.out.println("數據錯誤");
return;
}
// 定義輔助節點
Boy curBoy = null;
// 使用循環創建環形鏈表
for (int i = 1; i <= nums; i++) {
// 根據編號創建節點
Boy boy = new Boy(i);
// 如果是第一個節點
if (i == 1) {
first = boy;
first.setNext(first);
curBoy = first;// 讓curBoy指向第一個節點,幫助構建鏈表
} else {
curBoy.setNext(boy);
boy.setNext(first);// 使其指向第一個節點,形成環狀
curBoy = boy;// curBoy后移
}
}
}
// 遍歷當前環形鏈表
public void list() {
// 判斷鏈表是否空
if (first == null) {
System.out.println("鏈表為空");
return;
}
// 定義輔助節點
Boy curBoy = first;
while (true) {
System.out.println("節點編號:" + curBoy.getNo());
if (curBoy.getNext() == first) {
// 此時說明遍歷完畢
break;
}
curBoy = curBoy.getNext();// curBoy后移
}
}
}
//創建一個Boy類,表示一個節點
class Boy {
private int no;// 編號
private Boy next;// 指向下一個節點
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
這樣就實現了一個環形鏈表,接下來測試一下:
public static void main(String[] args) {
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
circleSingleLinkedList.list();
}
運行結果:
節點編號:1
節點編號:2
節點編號:3
節點編號:4
節點編號:5
運行結果也是沒有問題的,接下來便是生成出圈序列。
問題分析:
- 先創建一個輔助節點helper,事先應該指向環形鏈表的最后一個節點
- 報數前,先讓first和helper移動k - 1次
- 開始報數時,讓first和helper節點同時移動,移動m - 1次
- 此時可以將first指向的節點出圈
- 如何出圈呢?
使first = first.next,即:將first節點往前移動一下
使helper.next = first,這樣就跳過了要出圈的節點
接下來是代碼實現:
/**
* 根據用戶的輸入,計算出圈序列
*
* @param startNo 表示從第幾個開始數
* @param countNum 表示數幾下
* @param nums 表示一共有多少人
*/
public void countBoy(int startNo, int countNum, int nums) {
// 數據校驗
if (first == null || startNo < 1 || startNo > nums) {
System.out.println("參數輸入有誤");
return;
}
// 定義輔助節點
Boy helper = first;
// helper事先應該指向環形鏈表的最后一個節點
while (true) {
if (helper.getNext() == first) {
break;
}
helper = helper.getNext();// helper后移
}
// 報數前,先讓first和helper移動k - 1次
for (int j = 0; j < startNo - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
// 開始報數時,讓first和helper節點同時移動,移動m - 1次
// 這里是一個循環的操作,直到圈中只有一個節點
while (true) {
if (helper == first) {
// 此時說明圈中只有一個人
break;
}
// 讓first和helper同時移動countNum - 1次
for (int j = 0; j < countNum - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
// 此時first指向的節點就是要出圈的節點
System.out.println("節點" + first.getNo() + "出圈");
// 將該節點出圈
first = first.getNext();
helper.setNext(first);
}
System.out.println("最后留在圈中的節點編號:" + first.getNo());
}
這個實現的邏輯相對來說還是比較復雜和難以理解的,接下來編寫測試代碼:
public static void main(String[] args) {
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
circleSingleLinkedList.list();
System.out.println("--------------");
// 測試出圈序列
circleSingleLinkedList.countBoy(1, 2, 5);
}
運行結果:
節點編號:1
節點編號:2
節點編號:3
節點編號:4
節點編號:5
--------------
節點2出圈
節點4出圈
節點1出圈
節點5出圈
最后留在圈中的節點編號:3
和開始計算的結果相吻合。
到此,關於約瑟夫環的問題就成功解決了。