文西馬龍: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,這時候就發現程序的力量了,你人工去數,可想而知。
用程序來完成你能完成的事情,但是是快速的完成。
找工作的時候,很容易被考的問題:約瑟夫問題,排序和查找,二叉樹的遍歷,前序遍歷後序遍歷,霍夫曼數,最小堆等
有檔次的公司都喜歡考算法