韓順平_PHP程序員玩轉算法公開課(第一季)05_使用單鏈表解決約瑟夫問題_學習筆記_源代碼圖解_PPT文檔整理

文西馬龍:http://blog.csdn.net/wenximalong/

現在我們對單鏈表有了基本的瞭解,現在學習一下環形鏈表。
環形鏈表的內存示意圖

環形鏈表的好處:可以模擬許多實際的情景
如丟手帕問題,就是經典的用環形鏈表來解決的

現在我們來完成約瑟夫問題的解決方案!
Josephu問題

Josephu問題爲:射編號爲1,2,...n的n個人圍坐一圈,約定編號爲k(1<=k<=n)的人從1開始報數,數到m的那個人出列,它的下一位又從1開始報數,數到m的那個人又出列,依次類推,直到所有人出列爲止,由此產生一個出隊編號的序列。並求出最後出列的人是哪個?
提示:用一個不帶頭節點的循環鏈表來處裏Josephu問題,先構成一個有n個節點的單循環鏈表,然後由k節點起從1開始計數,計到m時,對應節點從鏈表中刪除,然後再從被刪除節點的下一個節點又從1開始計數,直到最後一個節點從鏈表中刪除算法結束。


思路:
(一)構建一個環形鏈表,鏈表上的每個節點,表示一個小朋友(PHP語言實現)


一個小朋友的情況

兩個小朋友的情況

當第一個結點做完以後,1號小朋友還有一個新的指向指向它,就是cur。cur的用處,當有2號小朋友的時候,cur就發揮作用了,此時cur還是指向1號小朋友,那麼$cur->next=$child;則①號線就被搭起來了,然後$child->next=$first;則②號線就被搭起來了,2號小朋友結點就指向了1號小朋友結點,環形鏈表就形成了。$cur=$cur->next;則cur就由原來的指向1號小朋友轉爲指向2號小朋友了,如圖中③號線所示,這樣就可以再加第3個小朋友了,依次類推。
(二)寫一個函數來顯示所有的小孩子
josephu.php

<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
	</head>
	<body>
		<h1>約瑟夫問題解決</h1>
		<?php
			//先構建一個環形鏈表,鏈表上的每個節點,表示一個小朋友
			//小孩類
			class Child{
				public $no;
				public $next=null;
				//構造函數
				public function __construct($no){
					$this->no=$no;
				}
			}
			//定義一個指向第一個小朋友的引用
			//定義一個空頭
			$first=null;
			$n=4; //$n表示有幾個小朋友
			//寫一個函數來創建一個四個小朋友的環形鏈表
			/*
			 addChild函數的作用是:把$n個小孩在構建成一個環形鏈表,$first變量就指向該環形鏈表的第一個小孩子
			 */
			function addChild(&$first,$n){ //此處要加地址符
				//1.頭結點不能動 $first不能動
				$cur=$first;
				for($i=0;$i<$n;$i++){
					$child=new Child($i+1); //爲什麼要加1,因爲for循環中i是從開始的,但是小朋友的編號是從1開始的
					//怎麼構成一個環形鏈表呢
					if($i==0){ //第一個小孩的情況
						$first=$child; //讓first指向child,但是還沒有構建環形鏈表
						$first->next=$child; //自己指向自己
						$cur=$first;
					}else{
						$cur->next=$child;
						$child->next=$first;
						//繼續指向下一個
						$cur=$cur->next;
					}
				}
			}
			//遍歷所有的小孩,並顯示
			function showChild($first){
				//遍歷 cur變量是幫助我們遍歷循環列表的,所以不能動
				$cur=$first;
				while($cur->next!=$first){ //cur沒有到結尾,就遍歷
					//顯示
					echo'<br/>小孩的編號是'.$cur->no;
					//繼續
					$cur=$cur->next;
				}
			}
			addChild($first,$n);
			showChild($first);
		?>
	</body>
</html>

爲什麼現在只顯示三個小孩呢,分析一下代碼在內存中是怎麼運作的

圖片大,在新窗口中打開圖片,觀看完整圖片


(1)語句①執行後,cur也指向了1號小朋友;
(2)然後語句②執行,進入while循環,此時cur指向1號小朋友,執行語句③,輸出:小孩的編號是1,再執行語句④$cur=$cur->next;
(3)此時cur指向2號小朋友,然後語句②執行,進入while循環,此時cur指向2號小朋友,執行語句③,輸出:小孩的編號是2,再執行語句④$cur=$cur->next;
(4)此時cur指向3號小朋友,,然後語句②執行,進入while循環,此時cur指向3號小朋友,執行語句③,輸出:小孩的編號是3,再執行語句④$cur=$cur->next;
(5)此時cur指向4號小朋友,然後執行語句②,此時$cur->next,就指向了1號小朋友,而first也指向1號小朋友,此時$cur->next!=$first;不成立了,爲假,就退出了while循環,因此就少了一個小孩,即4號小朋友。
所以要對josephu.php修改,即josephu2.php
josephu2.php

<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
	</head>
	<body>
		<h1>約瑟夫問題解決</h1>
		<?php
			//先構建一個環形鏈表,鏈表上的每個節點,表示一個小朋友
			//小孩類
			class Child{
				public $no;
				public $next=null;
				//構造函數
				public function __construct($no){
					$this->no=$no;
				}
			}
			//定義一個指向第一個小朋友的引用
			//定義一個空頭
			$first=null;
			$n=4; //$n表示有幾個小朋友
			//寫一個函數來創建一個四個小朋友的環形鏈表
			/*
			 addChild函數的作用是:把$n個小孩在構建成一個環形鏈表,$first變量就指向該環形鏈表的第一個小孩子
			 */
			function addChild(&$first,$n){ //此處要加地址符
				//1.頭結點不能動 $first不能動
				$cur=$first;
				for($i=0;$i<$n;$i++){
					$child=new Child($i+1); //爲什麼要加1,因爲for循環中i是從開始的,但是小朋友的編號是從1開始的
					//怎麼構成一個環形鏈表呢
					if($i==0){ //第一個小孩的情況
						$first=$child; //讓first指向child,但是還沒有構建環形鏈表
						$first->next=$child; //自己指向自己
						$cur=$first;
					}else{
						$cur->next=$child;
						$child->next=$first;
						//繼續指向下一個
						$cur=$cur->next;
					}
				}
			}
			//遍歷所有的小孩,並顯示
			function showChild($first){
				//遍歷 cur變量是幫助我們遍歷循環列表的,所以不能動
				$cur=$first;
				while($cur->next!=$first){ //cur沒有到結尾,就遍歷
					//顯示
					echo'<br/>小孩的編號是'.$cur->no;
					//繼續
					$cur=$cur->next;
				}
				//當退出while循環時,已經到了環形鏈表的最後,即cur此時指向最後一個小孩,所以還要處裏一下最後這個小孩節點
				echo'<br/>小孩的編號是'.$cur->no;
			}
			addChild($first,$n);
			showChild($first);
		?>
	</body>
</html>

=========


注意:function addChild(&$first,$n){ //此處$first前面爲什麼要加地址符&呢


修改的是值還是地址

test.php

<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
	</head>
	<body>
		<h1>function addChild(&$first,$n){ //此處$first前面爲什麼要加地址符&呢</h1>
		<?php
			class Dog{
				public $age;
				public function __construct($age){
					$this->age=$age;
				}
			}
			function test($dog){
				$dog->age=30;
			}
			//創建一個狗對象實例
			$dog1=new Dog(100);
			//調用test(調用函數就會開闢一個新棧)
			test($dog1);

			echo $dog1->age; //爲什麼會輸出30呢
			echo '<br/>';

			function test2($dog){
				$dog=new Dog(200);
			}
			//創建一個狗對象實例
			$dog1=new Dog(100);
			//調用test(調用函數就會開闢一個新棧)
			test2($dog1);
			echo $dog1->age; //此時輸出100還是200呢,輸出100
			echo '<br/>';

			//& 表示用地址傳遞
			function test3(&$dog){
				$dog=new Dog(200);
			}
			//創建一個狗對象實例
			$dog1=new Dog(100);
			//調用test(調用函數就會開闢一個新棧)
			test3($dog1);
			echo $dog1->age; //此時輸出100還是200呢,輸出200

		?>
	</body>
</html>

內存分析圖

圖片大,在新窗口中打開圖片,觀看完整圖片


(1)語句①執行完後,主棧裏就會有一個dog1變量,dog1變量指向了一個堆,對象實例的數據是放在堆區的;
(2)然後執行語句②,調用函數,不管哪種語言,只要調用函數,就會開闢一個新棧,在新棧test棧中,在test棧區中有一個變量 $dog,此$dog變量本身的地址是0x56,面向對象是引用傳遞的,$dog變量存的值是0x34,即$dog變量也指向堆區的0x34地址,當在test棧中執行$dog->age=30;這個語句後,因爲指向同一個地方,就把原來堆區中的100修改爲30,此時test函數就執行完畢,就返回了主棧,原來的test棧就消失了被回收了。
(3)接着在主棧執行語句③echo $dog1->age;,在主棧中$dog1指向堆區地址0x34,此時100已經被修改爲30了,所以最後輸出:30

再加深一下

內存分析圖

圖片大,在新窗口中打開圖片,觀看完整圖片


(1)語句①執行完後,主棧裏就會有一個dog1變量,dog1變量指向了一個堆,對象實例的數據是放在堆區的;
(2)然後執行語句②,調用函數,不管哪種語言,只要調用函數,就會開闢一個新棧,在新棧test2棧中,在test2棧區中有一個變量 $dog,此$dog變量本身的地址是0x56,面向對象是引用傳遞的,$dog變量存的值是0x34,即$dog變量也指向堆區的0x34地址,然後執行$dog=new Dog(200);此時就會在堆區開闢一個新的區間,地址爲0x88,存儲的值爲200,此時在test2棧中的$dog的0x34就會被修改爲0x88,此時的$dog變量就不再指向堆區的0x56了,而是指向了堆區地址0x88,,此時test2函數就執行完畢,就返回了主棧,原來的test2棧就消失了被回收了。(堆區地址0x34的值並沒有被修改)
(3)接着在主棧執行語句③echo $dog1->age;,在主棧中$dog1指向堆區地址0x34,此時100並沒有被修改,所以最後輸出:100

現在考慮加入地址符&的情況

內存分析圖

圖片大,在新窗口中打開圖片,觀看完整圖片


(1)語句①執行完後,主棧裏就會有一個dog1變量,dog1變量指向了一個堆,對象實例的數據是放在堆區的;
(2)然後執行語句②,調用函數,不管哪種語言,只要調用函數,就會開闢一個新棧,在新棧test3棧中,在test3棧區中有一個變量 $dog,此$dog變量本身的地址是0x56,在傳遞參數的時候,用的是&$dog,這個時候$dog變量存的值就是0x34,然後執行$dog=new Dog(200);此時就會在堆區開闢一個新的區間,地址爲0x88,存儲的值爲200,此時在test3棧中的$dog的0x34就會被修改爲0x88,主棧中$dog1的值0x34也會被同步修改爲0x88,此時新棧中的$dog變量和主棧中的$dog1變量都指向了堆區地址0x88,,此時test3函數就執行完畢,就返回了主棧,原來的test3棧就消失了被回收了。
(3)接着在主棧執行語句③echo $dog1->age;,在主棧中$dog1已經由指向堆區地址0x34轉爲指向堆區地址0x88,所以最後輸出:200
=========

(三)真正來玩遊戲
(1)先問題簡化,從第一個小孩開始數,數2,看看出列的順序

分析圖

現在我們已經有了環形鏈表,並且$first指向了1號小朋友節點,開始數,first就向下移動,2號小朋友節點出不了列,爲什麼呢?當$first指向2號小朋友節點,然後讓2號小朋友出列,是做不到的,如果是讓2號小朋友出列,是形成如下圖所示的環形列表,

讓1號小朋友的next指向3號小朋友,2號就出列了。但是這個$first是指向了2號本身。所以單鏈表的麻煩事情:★★它不能自己把自己刪除掉。
因此必須保證$first前面還有一個指針,如下圖所示,在$first的前面添加一個$tail指針

這個時候就可以把2號人物給刪除了,先讓$first先向走一步,讓$first指向3號小朋友,此時tail指向1號小朋友,讓tail的next指向$first,就是1號小朋友指向了3號小朋友,2號小朋友就出列了,如下圖所示

就是現在需要一個輔助指針,就是在運行之前,要先拿一個指針指向$first的前面,如下圖所示


-------------


josephu3.php

<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
	</head>
	<body>
		<h1>約瑟夫問題解決</h1>
		<?php
			//先構建一個環形鏈表,鏈表上的每個節點,表示一個小朋友
			//小孩類
			class Child{
				public $no;
				public $next=null;
				//構造函數
				public function __construct($no){
					$this->no=$no;
				}
			}
			//定義一個指向第一個小朋友的引用
			//定義一個空頭
			$first=null;
			$n=4; //$n表示有幾個小朋友
			//寫一個函數來創建一個四個小朋友的環形鏈表
			/*
			 addChild函數的作用是:把$n個小孩在構建成一個環形鏈表,$first變量就指向該環形鏈表的第一個小孩子
			 */
			function addChild(&$first,$n){ //此處要加地址符
				//1.頭結點不能動 $first不能動,$cur=$first;讓cur來幫我們穿針引線
				$cur=$first;
				for($i=0;$i<$n;$i++){
					$child=new Child($i+1); //爲什麼要加1,因爲for循環中i是從開始的,但是小朋友的編號是從1開始的
					//怎麼構成一個環形鏈表呢
					if($i==0){ //第一個小孩的情況
						$first=$child; //讓first指向child,但是還沒有構建環形鏈表
						$first->next=$child; //自己指向自己
						$cur=$first; //當$i==0的時候,就讓cur和first指向同一個地方
					}else{
						$cur->next=$child;
						$child->next=$first;
						//繼續指向下一個
						$cur=$cur->next;
					}
				}
			}
			//遍歷所有的小孩,並顯示
			function showChild($first){
				//遍歷 cur變量是幫助我們遍歷循環列表的,所以不能動
				$cur=$first;
				while($cur->next!=$first){ //cur沒有到結尾,就遍歷
					//顯示
					echo'<br/>小孩的編號是'.$cur->no;
					//繼續
					$cur=$cur->next;
				}
				//當退出while循環時,已經到了環形鏈表的最後,即cur此時指向最後一個小孩,所以還要處裏一下最後這個小孩節點
				echo'<br/>小孩的編號是'.$cur->no;
			}

			//問題簡化,從第一個小孩開始數,數2,看看出拳的順序
			function countChild($first){
				//思考:因爲我們每找到一個小孩,就要把他從環形鏈表中刪除
				//爲了能夠刪除某個小孩,我們需要一個輔助變量,該變量指向的小孩在$first前面,在$first的前面,因爲是環形的,就相當於在隊尾了。
				$tail=$first;
				while($tail->next!=$first){ //只要tail的next不等於first,就要tail不停的向下走,即$tail=$tail->next;
					$tail=$tail->next;
				}
				//上面的代碼,就是生成指向最後一個小孩的$tail
				//當退出while時,我們的$tail就指向了最後這個小孩

				//讓$first和$tail向後移動
				//移一下就相當於數了兩下,因爲還要數自己一下
				//如從1號小朋友到2號小朋友,只移動了一下,那麼是1號小朋友數1 再數2,因爲還數了自己一下。
				//移動2次,相當於數了3下,因爲自己數的時候,自己不需要動
				while($tail!=$first){ //當$tail==$first則說明只有最後一個人了
					for($i=0;$i<1;$i++){
						$tail=$tail->next;
						$first=$first->next;
					}
					//把$first指向的節點小孩刪除環形鏈表
					$first=$first->next;
					$tail->next=$first;
				}
				echo'<br/>最後留在圈圈的人的編號是'.$tail->no;
			}

			addChild($first,$n);
			showChild($first);

			//真正來玩遊戲
			countChild($first);
		?>
	</body>
</html>

(2)現在把數2做成變量m

josephu4.php

<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
	</head>
	<body>
		<h1>約瑟夫問題解決</h1>
		<?php
			//先構建一個環形鏈表,鏈表上的每個節點,表示一個小朋友
			//小孩類
			class Child{
				public $no;
				public $next=null;
				//構造函數
				public function __construct($no){
					$this->no=$no;
				}
			}
			//定義一個指向第一個小朋友的引用
			//定義一個空頭
			$first=null;
			$n=10; //$n表示有幾個小朋友
			//寫一個函數來創建一個四個小朋友的環形鏈表
			/*
			 addChild函數的作用是:把$n個小孩在構建成一個環形鏈表,$first變量就指向該環形鏈表的第一個小孩子
			 */
			function addChild(&$first,$n){ //此處要加地址符
				//1.頭結點不能動 $first不能動
				$cur=$first;
				for($i=0;$i<$n;$i++){
					$child=new Child($i+1); //爲什麼要加1,因爲for循環中i是從開始的,但是小朋友的編號是從1開始的
					//怎麼構成一個環形鏈表呢
					if($i==0){ //第一個小孩的情況
						$first=$child; //讓first指向child,但是還沒有構建環形鏈表
						$first->next=$child; //自己指向自己
						$cur=$first;
					}else{
						$cur->next=$child;
						$child->next=$first;
						//繼續指向下一個
						$cur=$cur->next;
					}
				}
			}
			//遍歷所有的小孩,並顯示
			function showChild($first){
				//遍歷 cur變量是幫助我們遍歷循環列表的,所以不能動
				$cur=$first;
				while($cur->next!=$first){ //cur沒有到結尾,就遍歷
					//顯示
					echo'<br/>小孩的編號是'.$cur->no;
					//繼續
					$cur=$cur->next;
				}
				//當退出while循環時,已經到了環形鏈表的最後,即cur此時指向最後一個小孩,所以還要處裏一下最後這個小孩節點
				echo'<br/>小孩的編號是'.$cur->no;
			}

			//爲了能夠刪除某個小孩,我們需要一個輔助變量,該變量指向的小孩在$first前面,在$first的前面,因爲是環形的,就相當於在隊尾了。
			$m=3;
			function countChild($first,$m){
				$tail=$first;
				while($tail->next!=$first){
					$tail=$tail->next;
				}
				//當退出while時,我們的$tail就指向了最後這個小孩

				//讓$first和$tail向後移動
				//移一下就相當於數了兩下,因爲還要數自己一下
				//如從1號小朋友到2號小朋友,只移動了一下,那麼是1號小朋友數1 再數2,因爲還數了自己一下。
				//移動2次,相當於數了3下,因爲自己數的時候,自己不需要動
				//移動m-1次,相當於數了m下 
				while($tail!=$first){ //當$tail==$first則說明只有最後一個人了
					for($i=0;$i<$m-1;$i++){
						$tail=$tail->next;
						$first=$first->next;
					}
					//把$first指向的節點小孩刪除環形鏈表
					$first=$first->next;
					$tail->next=$first;
				}
				echo'<br/>最後留在圈圈的人的編號是'.$tail->no;
			}

			addChild($first,$n);
			showChild($first);

			//真正來玩遊戲
			countChild($first,$m);
		?>
	</body>
</html>

(3)再考慮從第幾個人數的問題,變量k;和出列顯示

josephu5.php
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
	</head>
	<body>
		<h1>約瑟夫問題解決</h1>
		<?php
			//先構建一個環形鏈表,鏈表上的每個節點,表示一個小朋友
			//小孩類
			class Child{
				public $no;
				public $next=null;
				//構造函數
				public function __construct($no){
					$this->no=$no;
				}
			}
			//定義一個指向第一個小朋友的引用
			//定義一個空頭
			$first=null;
			$n=10; //$n表示有幾個小朋友
			//寫一個函數來創建一個四個小朋友的環形鏈表
			/*
			 addChild函數的作用是:把$n個小孩在構建成一個環形鏈表,$first變量就指向該環形鏈表的第一個小孩子
			 */
			function addChild(&$first,$n){ //此處要加地址符
				//1.頭結點不能動 $first不能動
				$cur=$first;
				for($i=0;$i<$n;$i++){
					$child=new Child($i+1); //爲什麼要加1,因爲for循環中i是從開始的,但是小朋友的編號是從1開始的
					//怎麼構成一個環形鏈表呢
					if($i==0){ //第一個小孩的情況
						$first=$child; //讓first指向child,但是還沒有構建環形鏈表
						$first->next=$child; //自己指向自己
						$cur=$first;
					}else{
						$cur->next=$child;
						$child->next=$first;
						//繼續指向下一個
						$cur=$cur->next;
					}
				}
			}
			//遍歷所有的小孩,並顯示
			function showChild($first){
				//遍歷 cur變量是幫助我們遍歷循環列表的,所以不能動
				$cur=$first;
				while($cur->next!=$first){ //cur沒有到結尾,就遍歷
					//顯示
					echo'<br/>小孩的編號是'.$cur->no;
					//繼續
					$cur=$cur->next;
				}
				//當退出while循環時,已經到了環形鏈表的最後,即cur此時指向最後一個小孩,所以還要處裏一下最後這個小孩節點
				echo'<br/>小孩的編號是'.$cur->no;
			}

			//爲了能夠刪除某個小孩,我們需要一個輔助變量,該變量指向的小孩在$first前面,在$first的前面,因爲是環形的,就相當於在隊尾了。
			$m=3;
			$k=2;
			function countChild($first,$m,$k){
				$tail=$first;
				while($tail->next!=$first){
					$tail=$tail->next;
				}

				//考慮是從第幾個人開始數的問題,變量k
				//首先要定位
				//因爲動一下,就相當於數了兩下,算上自己了,所以k-1
				for($i=0;$i<$k-1;$i++){
					$tail=$tail->next;
					$first=$first->next;
				}

				//當退出while時,我們的$tail就指向了最後這個小孩

				//讓$first和$tail向後移動
				//移一下就相當於數了兩下,因爲還要數自己一下
				//如從1號小朋友到2號小朋友,只移動了一下,那麼是1號小朋友數1 再數2,因爲還數了自己一下。
				//移動2次,相當於數了3下,因爲自己數的時候,自己不需要動
				//移動m-1次,相當於數了m下 
				while($tail!=$first){ //當$tail==$first則說明只有最後一個人了
					for($i=0;$i<$m-1;$i++){
						$tail=$tail->next;
						$first=$first->next;
					}
					//把$first指向的節點小孩刪除環形鏈表
					//出列顯示,應該在first沒有改變之前先打印輸出
					echo'<br/>出圈的人的編號是'.$first->no;
					$first=$first->next;
					$tail->next=$first;
				}
				echo'<br/>最後留在圈圈的人的編號是'.$tail->no;
			}

			addChild($first,$n);
			showChild($first);

			//真正來玩遊戲
			countChild($first,$m,$k);
		?>
	</body>
</html>

關鍵是要在腦海中,有一個內存分佈和運行的大致圖。
一旦人多了,比如4000個人,從第31個人,數20,這時候就發現程序的力量了,你人工去數,可想而知。
用程序來完成你能完成的事情,但是是快速的完成。
找工作的時候,很容易被考的問題:約瑟夫問題,排序和查找,二叉樹的遍歷,前序遍歷後序遍歷,霍夫曼數,最小堆等
有檔次的公司都喜歡考算法

韓順平_PHP程序員玩轉算法公開課_學習筆記_源代碼圖解_PPT文檔整理_目錄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章