1. 程式人生 > >漢諾塔遞迴的c語言實現(遞迴)

漢諾塔遞迴的c語言實現(遞迴)

     對於遞迴來講, 漢諾塔實際是經典到不能再經典的例子了,   每個資料結構的教材對會提到.

     但是到最後只給出一段類似下面的一段程式碼:

#include<stdio.h>
 
void move(int n,char a,char b,char c)
{
    if(n==1)
        printf("\t%c->%c\n",a,c);    //當n只有1個的時候直接從a移動到c 
    else
    {   
        move(n-1,a,c,b);            //把a的n-1個盤子通過c移動到b 
        printf("\t%c->%c\n",a,c);   //把a的最後1個盤(最大的盤)移動到c 
        move(n-1,b,a,c);            //吧b上面的n-1個盤通過a移動到c 
    }   
}
 
main()
{
    int n;
    printf("請輸入要移動的塊數:"); 
    scanf("%d",&n);
    move(n,'a','b','c');
}

     這段程式碼用來學習遞迴思想是足夠的,  也能大概上了解到移動漢諾塔的步驟.

     但是我說, 這段程式碼沒有體會出漢諾塔的本質, 也沒有辦法驗證.

      下面我會敲出能體現出漢諾塔本質的遞迴程式碼. 並能根據輸出每1個步漢諾塔的狀態, 並且驗證正確性.

1. 什麼是漢諾塔

 下面的定義摘自維基百科:

有三根杆子A,B,C。A杆上有N個(N>1)穿孔圓盤,的尺寸由下到上依次變小。要求按下列規則將所有圓盤移至C杆:

  1. 每次只能移動一個圓盤;
  2. 盤不能疊在盤上面。

配圖:

2. 漢諾塔的本質是3個棧

維基的定義只簡單提到了漢諾塔的規則, 但是並沒有揭示它的本質.

   下面我們來分析它的本質.

      1.每次只能移動1個盤:     

      也就說不能兩個盤一齊移動, 必須按順序1個1個套在柱子上,  而且只能從柱子的上方套入, 也能只能從柱子的上方取出.

      這明顯就是1個先進後出的線性結構了,  因為出入口只有1個啊, 柱子的下方是不能放入和取出盤子的.

      先進後出的線性結構就是棧了,  套入柱子和取出盤子就是對應的壓棧和出棧動作.   如果讀者之前沒有了解過棧的話, 個人建議先去了解下棧, 然後再往下看.

      2. 大盤不能套在小盤上面

      代表這3個棧中, 如果不是空棧, 那麼壓棧的元素必須比棧頂元素小, 然後才允許壓棧.   這就保證棧裡面的元素是從小到大排序的.

      總結:  漢諾塔的本質就是3個棧,  而且壓棧的元素必須比棧頂元素(如果存在)小.

3. 漢諾塔的解題思路及遞迴原理

     好, 現在開始講解漢諾塔的解題思路.

     假如A塔有n個盤子, 大小從大(底部) 到小(頂部)排列,  B塔和C塔都是空塔.

     如下圖:


好了那麼到底如何把這個n個盤子移動到C塔呢?

3.1  假如n=1 也就是說A塔只有1個盤子的情況下, 直接將1個盤子移動到c塔

那麼我們寫1個函式move()

執行move(A,C) 就是把A裡唯一1個盤子移動到C

當然這裡先不管這個函式具體是如何實現的.

但是可看出, 這個過程是不需要藉助B塔中轉的.

3.2  n>1的情況分析, hanoi_m(A,B,C,n) 函式

但是n>1呢? 上面的move函式就行不同了, 所以我們需要1個新的函式 hanoi_m()

hanoi_m(A, B, C, n) 函式意思就是把n個盤子從A藉助B移動到C.

這裡也先不管這個函式是如何具體如何實現, 知道它的引數意義和目地就ok了.

我上篇文章講過, 遞迴的條件之一就是令資料規模不斷地減少, 也就是假如1個函式f(n) 是遞迴函式, 則必須要找出f(n) 與f(n-1)關係,  也就是說把求f(n) 轉化為求f(n-1), 然後再轉化為求f(n-2), 最終f(1)就是出口.

那麼這裡的hanoi_m(x,x,x,1) 是什麼呢?  就是上面的move()函數了,  也就說出口找到了.

但是hanoi_m(x,x,x,n) 與 hanoi_m(x,x,x, n-1) 的關係還不知道,  這就是漢諾塔遞迴函式的精髓了!

下面步驟就是講解這個n 與 n-1的關係.

關鍵的一點:  假如要將n-1個盤子放到C塔, 如果c塔是非空的話, 那麼c塔的盤子都必須比n-1大, 否則按照規則是不能放入的!

所以 最大的盤子必須比 它小的所有盤子先放入C塔. 

1.但是如果要將最大的盤子從A塔移動到C塔,  那麼C塔必須是空的, 不讓放不下

2.而且最大的盤子必須在A塔的最上面, 也就是說A塔只有1個最大的盤子.

3.所以其他盤子都必須在B塔上,

理解好這個那麼問題就不大了

下面是詳細步驟

3.3  第一步, 將A上面的n-1個盤子藉助C塔移動到B塔. hanoi(A,C,B,n-1)

這個過程用函式來表示就是hanoi_m(A,C,B,n-1)啊, 理解這一步也十分關鍵:

如圖:


至於這個過程具體如何實現, 這裡也不用去管, 只有1點是明確的, 只要n-1>1 那麼這個過程肯定需要C塔來中轉, 而且肯定可以化為求 hanoi_m(x,x,x, n-2).

假如n-1=1? 就直接move(A,B)就ok了, 這就是出口啊.

3.4  第二步, 將A上面的最後的(最大的)盤子移動到C塔 move(A,C)

  因為現在A塔了只有1個最大盤子n了啊, 所以無需藉助B塔, 直接move(A,C)搞定

如下圖:


注意現在還沒完成哦

3.4  第三步, 將B上面的所有盤子(n-1)個盤子藉助A塔移動到C塔 hanoi(B,A,C,n-1)

因為C塔的盤子比B塔的所有盤子都大, 所以是可以忽略掉C盤的那個最大的盤子的, 理解這個也很重要啊.

這個函式實現就是 hanoi(B,A,C,n-1)

如圖:


搞掂完成了...

3.5  總結及偽演算法:

經過上面的圖解, 我們知道hanoi(n) 與hanoi(n-1)的關係了, 可以分成三步啊

偽演算法就是:

hanoi_m(A,B,C,n){
        if (n==0){
                move(A,C);
        }
        hanoi_m(A,C,B,n-1);
        move(A,C);
        hanoi_m(B,A,C,n-1);
}

看看這個函式, 自己呼叫了自己, 而且規模不斷在減少, 還有1個明確的出口, 標準的遞迴函式啊.

再看看本文開始那個函式, 是不是能理解了

然而 真正的程式碼實現沒那麼簡單,  但是起碼邏輯已經清楚了.



4. 1個靜態棧(陣列核心)的容器的c語言程式碼實現

     假設我們可以用類似下面的程式碼來定義3個棧, 和呼叫壓棧和出棧函式, 就很方便了.

//declare a stucks
stuck A = new stuck;

//push
A->push(1);  //push element int "1" into the stuck

//pop
int i;
A->pop(&i) //pop the top element, and assign the value to variable i

咋一看不是c++的程式碼嗎?, 的確c語言不能實現面向物件的類.

但是利用c語言裡的結構體和函式指標, 也可以實現類似上面程式碼的功能.

用c語言寫1個靜態棧容器並不是很簡單, 可以說遠比漢諾塔的函式複雜,  具體的程式碼我就不講解了. 有興趣的可以參考我之前的文章, 有1個動態棧(連結串列核心)的c語言程式碼例子講解.

但是靜態棧的原理遠比動態棧複雜.

下面我只會介紹下標頭檔案的函式, 具體當面我會作為附件, 有興趣的可以下載:

#include "bool_me.h"
#ifndef __ARRSTUCK1_H_
#define __ARRSTUCK1_H_

	struct arrstuck1{
		int * pArr;  //address of array (int type)
		int arrlen;  //the maxlen of th array
		int top; //index of the top + 1
		int buttom; //index of buttom , default is 0
		const char * stname; //name of stuck, haha ,used to print the log to logfile
		int (* len)(struct arrstuck1 *);   //get the length
		int (* TopVal)(struct arrstuck1 *);  //get the top value
		int (* ButtomVal)(struct arrstuck1 *);  //get the top value
		BOOL (* is_empty)(struct arrstuck1 *);
		BOOL (* is_full)(struct arrstuck1 *);
		BOOL (* push)(struct arrstuck1 *, int);   //push an element to the stuck
		BOOL (* pop)(struct arrstuck1 *, int *); //pop the topelemnt to the stuck
		void (* print)(struct arrstuck1 *); //print from buttom to top
		void (* print_from_top)(struct arrstuck1 *); //print from top to buttom
		void (* clean)(struct arrstuck1 *); //remove all elements
		BOOL is_inited; //judge whether the arraystuck is initialed
	};

	typedef struct arrstuck1 INT_STUCK;

	//initail
	INT_STUCK * ast_int_new();
	//free
	BOOL ast_free(INT_STUCK *);

#endif /* __ARRSTUCK1_H_ */

上面就是靜態棧容器 INT_STUCK(定義別名) 的標頭檔案, 它只可以存放int型別的元素.

我們只需要關係幾個關鍵的成員函式.

1.  push()  函式 就是壓棧啦

2. pop() 函式 出棧

3. print() 列印棧裡面的所有元素, 用於驗證演算法的正確性啦.

4. len()  棧裡面元素的個數.

5. ast_int_new5.  in() 初始化函式, 不初始化沒法用的.

其他的可以看註釋了.

具體的函式定義請下載來看:

因為這篇文章的注意目地是分析漢諾塔的遞迴函式嘛..

5. 漢諾塔遞迴c語言實現

      好了, 到此為止, 準備動作做完了, 下面開始分析和編寫漢諾塔的函式

5.1  定義單個漢諾塔型別 及 其初始化函式

     上面講過了, 漢諾塔的本質就是棧,, 所以漢諾塔型別就是棧啦, 用typedef 函式起個別名就ok了

     利用函式指標可以為函式起別名

程式碼如下:

typedef INT_STUCK HANOITOWER;
static HANOITOWER * (* hanoi_new)() = ast_int_new;

這樣就定義了1個"新"的型別  HANOITOWER,  和初始化函式 hanoi_new()

5.2  漢諾塔放盤子函式hanoi_push

實際上就是棧的壓棧函式,  當然, 這裡要判斷入棧的值必須比棧頂小!

程式碼如下:

BOOL hanoi_push(HANOITOWER * pIst, int val){
	if (TRUE != pIst->is_empty(pIst) && val >= pIst->TopVal(pIst)){
		printf("val is greater than top!\n");
		return FALSE;
	}

	pIst->push(pIst, val);
	return TRUE;
}


5.3  定義3個漢諾塔棧 A,B,C 並往A裡面放4個盤子

有了上面的函式, 就可以初始化3個塔, 並往A塔放4個盤子了, 當然B,C塔必須是空塔(空棧)

當然放幾個盤子自己定義, 建議不要放太多啦, 移動盤子動作增長速度很恐怖的. 反正我的渣機子算24個盤子要5分鐘才出結果

程式碼如下:

        HANOITOWER * pTa = hanoi_new();
	pTa->stname = "TowerA";
	HANOITOWER * pTb = hanoi_new();
	pTb->stname = "TowerB";
	HANOITOWER * pTc = hanoi_new();
	pTc->stname = "TowerC";

	int i;

	for (i=4; i >= 1; i--){
		hanoi_push(pTa, i);
	}

5.4 從非空漢諾塔取盤子函式 hanoi_pop

沒錯, 本質上就是出棧函式啊, 拿出棧頂元素(用於放到別的棧)

所以要接受1個 int 型別的指標引數,  用於存放和取出這個棧頂啊.

程式碼如下:

BOOL hanoi_pop(HANOITOWER * pIst, int * pVal){
	if (TRUE == pIst->is_empty(pIst)){
		printf("fail to pop as the stuck is empty!\n");
		return FALSE;
	}

	pIst->pop(pIst, pVal);
	return TRUE;
}

5.5  把1個盤子從1個塔移動到另1個塔函式 hanoi_move

沒錯, 實際上我們操作漢諾塔, 不會單獨地取盤子和放盤子,   而是把1個塔的頂部盤子拿出來 放到另1個塔的頂部!

所以 這個函式Hanoi_move 的引數有兩個棧, T_from 和 T_to

實際上是分解上出棧和壓棧函式

從T_from出棧 並獲得出棧的元素(盤子), 然後把這個元素壓棧到T_to中.

程式碼如下:

BOOL hanoi_move(HANOITOWER * pIst_from, INT_STUCK * pIst_to){
	int val;
	if (TRUE == hanoi_pop(pIst_from, &val)){
		if (TRUE == hanoi_push(pIst_to, val)){
			//mark log to file
			sprintf(hanoi_move_str, "\nmove %d from %s to %s\n", val, pIst_from->stname, pIst_to->stname);
			base_log(hanoi_move_str, HANOI_OP_FILE, "a");
			base_log_intarr(pIst_from->stname, pIst_from->pArr, pIst_from->len(pIst_from), HANOI_OP_FILE, "a");
			base_log_intarr(pIst_to->stname, pIst_to->pArr, pIst_to->len(pIst_to), HANOI_OP_FILE, "a");
			return TRUE;
		}
	}

	return FALSE;
}


見到, 如果移動成功, 會把移動盤子的資訊(從哪裡移動到哪, 移動哪個元素) 記錄在日誌檔案, 而且記錄每1個移動步驟後, 兩個塔裡面的元素狀態.

其中hanoi_move_str 是外部定義公共變數

5.6  輸出單個塔裡面的元素函式 hanoi_print

實際上就是輸出棧裡面的所有元素啦, 用於驗證嘛..

..

void hanoi_print(HANOITOWER * pIst){
	pIst->print(pIst);
}


5.7  解題遞迴函式 hanoi_m

原理和偽演算法上面都講過了啦, 不是嗎?  而且有了上面的程式碼準備, 現在寫這個遞迴函式就十分簡單了.

程式碼如下:

int hanoi_m(HANOITOWER * pfrom, HANOITOWER * pmid, HANOITOWER *pto, int count){
	if (count == 1){
		hanoi_move(pfrom, pto);
		return 0;
	}

	hanoi_m(pfrom,pto,pmid,count-1);
	hanoi_move(pfrom, pto);
	hanoi_m(pmid,pfrom,pto,count-1);

	return 0;
}


6. 測試這個漢諾塔程式碼

好了, 所有函式都寫好了, 就寫1個測試程式驗證啊,  n先設為4嘛, 不讓日誌太長了..

程式碼如下:

int hanoi1(){
	HANOITOWER * pTa = hanoi_new();
	pTa->stname = "TowerA";
	HANOITOWER * pTb = hanoi_new();
	pTb->stname = "TowerB";
	HANOITOWER * pTc = hanoi_new();
	pTc->stname = "TowerC";

	hanoi_move_str = (char *)malloc(sizeof(char) * 50);
	int i;

	for (i=4; i >= 1; i--){
		hanoi_push(pTa, i);
	}

	printf("before move\n");
	printf("tower A is below\n");
	hanoi_print(pTa);
	printf("tower B is below\n");
	hanoi_print(pTb);
	printf("\ntower C is below\n");
	hanoi_print(pTc);

	base_log("start to move\n", HANOI_OP_FILE, "w");

	hanoi_m(pTa, pTb, pTc, pTa->len(pTa));

	printf("\nafter move\n");
	printf("tower A is below\n");
	hanoi_print(pTa);
	printf("\ntower B is below\n");
	hanoi_print(pTb);
	printf("\ntower C is below\n");
	hanoi_print(pTc);

	ast_free(pTa);
	ast_free(pTb);
	ast_free(pTc);

	free(hanoi_move_str);
	printf("hanoi_new done\n");
	return 0;
}


注意我先移動前先輸出3個塔的元素, 移動後再輸出1次, 就可以驗證了嘛..

輸出:  注意看棧的輸出元素.


我上面是不是說了每1次移動我都會記錄在日誌檔案中:

開啟來看看就可以知道每一次移動的作用和意義了, 能加深理解哦:

[email protected] c_start $ cat ~/tmp/HANIO_OP_FILE.log 
start to move

move 1 from TowerA to TowerB
TowerA: 4, 3, 2
TowerB: 1

move 2 from TowerA to TowerC
TowerA: 4, 3
TowerC: 2

move 1 from TowerB to TowerC
TowerB: blank array!
TowerC: 2, 1

move 3 from TowerA to TowerB
TowerA: 4
TowerB: 3

move 1 from TowerC to TowerA
TowerC: 2
TowerA: 4, 1

move 2 from TowerC to TowerB
TowerC: blank array!
TowerB: 3, 2

move 1 from TowerA to TowerB
TowerA: 4
TowerB: 3, 2, 1

move 4 from TowerA to TowerC
TowerA: blank array!
TowerC: 4

move 1 from TowerB to TowerC
TowerB: 3, 2
TowerC: 4, 1

move 2 from TowerB to TowerA
TowerB: 3
TowerA: 2

move 1 from TowerC to TowerA
TowerC: 4
TowerA: 2, 1

move 3 from TowerB to TowerC
TowerB: blank array!
TowerC: 4, 3

move 1 from TowerA to TowerB
TowerA: 2
TowerB: 1

move 2 from TowerA to TowerC
TowerA: blank array!
TowerC: 4, 3, 2

move 1 from TowerB to TowerC
TowerB: blank array!
TowerC: 4, 3, 2, 1

看到n=4 的話 執行了 15次move動作

注意n不要設成太大啊,   不然這個日誌會有成千上萬行的啊!

分析一下, 從n =1 到n =5 的5個樣本

move分別執行了

1 3 7 15 31 ....  (2-1,4-1,8-1,16-1

也就是隨著n增長,  move執行次數= 2^n-1

可以看出, 隨著n的增長, move的執行次數增長是幾何級數的節奏啊!! 所以從時間複雜度O(2^n)來看, 這個遞迴函式是非常糟糕的. 但是畢竟容易理解和易於實現啊.

到底有多糟糕,  我將n設為24, 執行了8分鐘才出結果,

move執行了, 1677多萬次

日誌檔案6700多萬行,  大小高達1.6 GB,  真是瞎了我的狗眼.

最後附上這個漢諾塔程式的程式碼:

和上面的靜態棧檔案1齊編譯就能執行了



相關推薦

問題(C語言程式碼)

漢諾塔:漢諾塔(Tower of Hanoi)源於印度傳說中,大梵天創造世界時造了三根金鋼石柱子,其中一根柱子自底向上疊著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。 有三根杆子A

c語言實現()

     對於遞迴來講, 漢諾塔實際是經典到不能再經典的例子了,   每個資料結構的教材對會提到.      但是到最後只給出一段類似下面的一段程式碼: #include<stdio.h> void move(int n,char a,char b,cha

C語言實現的快速排序

C語言實現遞迴的快速排序 目錄 文章目錄 C語言實現遞迴的快速排序 目錄 原理 程式碼 原理 原理這裡就不多闡述了,主要就是使用頭尾兩個指標,對待排序陣列進行操作,遞迴的收斂返

關於的一些個人看法(

漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動

:最少移動次數&&路徑

漢諾塔: 三個柱子:A,B,C,A有n個環,講n個環全部移動到C上,要求: 1>  移動次數最少; 2>  大環不能放在小環上。 輸入: n(n<=10) 輸出 : 移動次數 路徑

如何使用C語言實現呼叫

遞迴的定義: 程式呼叫自身的程式設計技巧稱為遞迴,就是執行時呼叫了自己。 什麼樣的問題適合使用遞迴方式: 如果一個大問題可以拆分成幾個小問題,其中有n個小問題和原來的大問題本質一樣,只是難度小一些。這種問題可以考慮採用遞迴的方式解決。 遞迴函式的編碼原則: 1、編寫語句

迭代和(Python)--乘方、最大公約數、、斐波那契、文字串

1.迭代 def iterPower(base,exp): result=1.0 while exp>0: result*=base exp-=1 return result 執行結果: 2.遞迴的乘法運算:

搬運過程的python實現(原創

法國數學家愛德華·盧卡斯曾編寫過一個印度的古老傳說:在世界中心貝拿勒斯的聖廟裡,一塊黃銅板上插著三根寶石針。印度教的主神梵天在創造世界的時候,在其中一根針上從下到上地穿好了由大到小的64片金片,這就是所謂的漢諾塔。不論白天黑夜,總有一個僧侶在按照下面的法則移動這

問題求解的Python實現

問題描述: 有三根杆(編號A、B、C),在A杆自下而上、由大到小按順序放置n個盤(詳細的圖,自己查詢) 遊戲的目標: 把A杆上的金盤全部移到C杆上,並仍保持原有順序疊好。 操作規則: 每次只能移動一個盤子,並且在移動過程中三根杆上都始終保持大盤在下,小盤在上,操作過程中盤子可以置於A、B、C任一杆上

呼叫(C語言實現

1.遞迴演算法 遞迴演算法:是一種直接或者間接地呼叫自身的演算法。在計算機編寫程式中,遞迴演算法對解決一大類問題是十分有效的,它往往使演算法的描述簡潔而且易於理解。 遞迴過程一般通過函式或子過程來實現。 遞迴演算法的實質:是把問題轉化為規模縮小了的同類問題的子問題。然後

c語言實現

程式碼不是自己寫的,copy資料結構書上的,看的懂,但是寫不出來。 //程式碼很簡潔,但卻是經典 #include <stdio.h> int count =0; void move(char x,int n,char y) {     co

【經典問題】(C語言實現)

   這個月在學習Python,函式遞迴那一節提到了漢諾塔問題,這是一個很經典的問題,我們老師在之前也提到過這個例子,感覺蠻重要的,想記錄下來。為了向經典致敬,這篇就不用Python實現了,就用C語言~  漢諾塔的來歷我就不細說了,這裡就自行左擁度娘右抱谷歌腦補一下。請看下面

C語言程式碼實現

#include <stdio.h> int c=0; void move(int disk,char start,char end) { printf("step:%d,move

[] - C語言

#include<stdio.h> //將n個盤子從x藉助y移動到z void move(int n,char x,char y,char z) { if (1==n) printf("%c-->%c\n",x,z); else { move(n-1,x,

C語言--演算法

問題描述:   有一個梵塔,塔內有三個座A、B、C,A座上有諾幹個盤子,盤子大小不等,大的在下,小的在上(如圖)。 把這些個盤子從A座移到C座,中間可以借用B座但每次只能允許移動一個盤子,並且在移動過程中,3個座上的盤 子始終保持大盤在下,小盤在上。 描述簡化:把

(Hanoi)問題&非C++實現及總結

漢諾塔(Hanoi)問題遞迴&非遞迴的C++實現及總結 由於剛入門不算很久,所以就那漢諾塔這種簡單問題來練下手啦~~ 【漢諾塔問題內容】(雖然路人皆知但還是寫一下好了。。。)   相傳在古印度聖廟中,有一種被稱為漢諾塔(Hanoi)的遊戲。

C++_實現

A為存放盤子的塔,B為目標塔,C為輔助塔 演算法分為三步 一、將A上n-1個盤子全部放到C塔上 二、將A上剩下的一個盤子放到B塔上 三、將C塔上的盤子全部放到B塔上 注:不需要考慮如何移動n-

C語言解決(hanoi)問題——函式的呼叫

#include <stdio.h> void main() { void hanoi(int n,char one,char two,char three); int n; printf("請輸入需要移動的盤子數:\n"); scanf("%d",&n

的故事(C語言——歸)

code log 圓盤 印度 return 16px move class baidu 漢諾塔:   漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤