1. 程式人生 > >經典演算法--約瑟夫環問題的三種解法

經典演算法--約瑟夫環問題的三種解法

約瑟夫環問題,這是一個很經典演算法,處理的關鍵是:偽連結串列

問題描述:N個人圍成一圈,從第一個人開始報數,報到m的人出圈,剩下的人繼續從1開始報數,報到m的人出圈;如此往復,直到所有人出圈。(模擬此過程,輸出出圈的人的序號)

在資料結構與演算法書上,這個是用連結串列解決的。我感覺連結串列使用起來很麻煩,並且這個用連結串列處理起來也不是最佳的。

我畫了一個圖用來理解:


有如下問題需要首先考慮:

1、“圈”怎樣形成?

    以上圖為例:下標從0開始,當下標為11的時候,在加1,就應該回到0。

    index = (index+1) % count;

2、怎樣處理?陣列or連結串列or其他方法?

    連結串列使用起來很笨重,我們有迴圈陣列的方法。

解法一程式分析:

迴圈的開始和結束:迴圈的結束取決於圈內是否還有“人”,可以用一個變數alive表示初始人數,每一次出圈,alive-1.判斷alive是否非0即可。

    while(alive > 0)

每一次迴圈,就是“過”一個人,但是,這個人有兩種不同的狀態:在圈內和不在圈內;在圈內就報數,number+1,不在圈內就不參與報數,number不變。

假設有N個int元素的陣列,每一個int元素表示一個“人”;並且,取值為0和1, 1表示在圈內,0表示不在圈內,所以,如果這個人在圈內,number+1;如果這個人不在圈內,number+0。那麼,在報數的時候,不需要考慮這個人在不在圈內就行(每一個人都需要加1或加0,所以,可以在這塊優化一下程式)。

void joseph(int count, int doom) {
	int alive = count;		//倖存人數 
	int number = 0;			//計數,當number==doom時,淘汰這個人 
	int index = 0;			//下標,為總人數-1 
	int *circle = NULL;		//根據需求設為迴圈陣列,儲存每個人 

	//用calloc()函式申請得到的空間,自動初始化每個元素為0
	//所以,0表示在這個人在約瑟夫環內,1表示這個人出圈,即“淘汰” 
	circle = (int *) calloc(sizeof(int), count);

	//只要倖存人數大於0,則一直進行迴圈 
	while(alive > 0) {
		number += 1- circle[index];	//每輪到一個人報數,不管是"0"還是"1"都進行計數 
		if(number == doom) {		//當number==doom時,就要淘汰當前這個人
			/*
				淘汰一個人需要做四步操作:
					1、輸出這個人的位置 
					2、把這個人的狀態從在圈內"0"改為不在圈內"1" 
					3、倖存人數alive-- 
					4、 計數器number歸零 
			*/ 
			alive == 1 ? printf("%d", index+1) : printf("%d,", index+1);
			circle[index] = 1;
			alive--;
			number = 0;
		}
		//與總人數count取餘,則可以使index在0~count-1之間 一直迴圈,達到迴圈陣列的目的 
		index = (index +1) % count;	
	}
	printf("\n");

	free(circle);		//結束後一定要釋放circle所申請的空間 
}


解法二程式分析:

解法二在解法一的基礎上進行了優化,對出圈的人的節點進行刪除,可以減少時間複雜度。

假設,要刪除的節點下標為curIndex,其前驅節點下標為preIndex;

    circle[preIndex] = circle[curIndex];

    假設要刪除的節點下標curIndex=3,那麼circle[2] = circle[3] ,circle[2]  = 4,circle[2]之前等於3,現在等於4。即下標為3的第四個人已經被刪除了。

怎樣遍歷?

    preIndex = curIndex;

    curIndex = circle[curIndex];

但是,在出圈的時候,curIndex 和preIndex 的變化有別於上面的操作:

出圈時,curIndex 需要後移,preIndex 應該不動!

每次迴圈,直接報數,因為被刪除的人(例如上面的第四個人)不可能進行報數了。

void joseph(int count, int doom) {
int alive = count;				// 倖存人數
	int number = 0;				// 報數的數
	int curIndex = 0;			// 當前人下標
	int preIndex = count - 1;   // 前一個人下標
	int *circle = NULL;
	int index;

	circle = (int *) malloc (sizeof(int) * count);
	//對circle陣列進行初始化 
	for(index = 0; index < count; index++) {
		circle[index] = (index + 1) % count;
	}

	while(alive > 0) {
		number++;
		if(number == doom) {
			alive == 1 ? printf("%d", curIndex+1) : printf("%d,", curIndex+1); 
			alive--;
			number = 0;
			circle[preIndex] = circle[curIndex];	//出圈操作 
		} else {
			preIndex = curIndex;	//處理下一個人 
		}
		curIndex = circle[curIndex];
	}

	free(circle);
}


解法三程式分析:

解法三裡沒有進行number報數,而是直接計算出需要移動的人數,然後定位到要出圈的人。

num = doom % alive - 1;

for(index = 0; index < (num == -1 ? alive - 1 : num); index++) {
preIndex = curIndex;
curIndex = circle[curIndex];
}

void joseph(int count, int doom) {
	int alive = count;	// 倖存人數
	int curIndex = 0;			// 當前人下標
	int preIndex = count - 1; // 前一個人下標
	int *circle = NULL;
	int index;
	
	circle = (int *) malloc(sizeof(int) * count);
	for(index = 0; index < count; index++) {
		circle[index] = (index + 1) % count;	// 初始化連結串列
	}
	
	while(alive > 0) {	// 只要還有幸存者,就繼續“殺”
		int num = doom % alive - 1; // 直接計算出需要移動的人數,
		// 直接定位到要出圈的人
		for(index = 0; index < (num == -1 ? alive - 1 : num); index++) {
			preIndex = curIndex;
			curIndex = circle[curIndex];
		}
		// 該人出圈!
		alive == 1 ? printf("%d", curIndex+1) : printf("%d,", curIndex+1);  
		alive--;
		circle[preIndex] = circle[curIndex]; // 真正的出圈操作!
		curIndex = circle[curIndex]; // 繼續處理下一個人
	}
	// 這個演算法比normalJoseph.c效率提高30%!
	
	free(circle);
}


GitHub原始碼地址:

https://github.com/yangchaoy259189888/JosephRing/