數據結構開發(15):遞歸的思想與應用
阿新 • • 發佈:2018-12-20
signed bject bool 全排列問題 繼續 定義 2.4 rev --
0.目錄
1.遞歸的思想
2.遞歸的應用
- 2.1 單向鏈表的轉置
- 2.2 單向排序鏈表的合並
- 2.3 漢諾塔問題
- 2.4 全排列問題
- 2.5 逆序打印單鏈表中的偶數結點
- 2.6 八皇後問題
3.小結
1.遞歸的思想
遞歸是一種數學上分而自治的思想:
- 將原問題分解為規模較小的問題進行處理
- 分解後的問題與原問題的類型完全相同,但規模較小
- 通過小規模問題的解,能夠輕易求得原問題的解
- 問題的分解是有限的 ( 遞歸不能無限進行 )
- 當邊界條件不滿足時,分解問題 ( 遞歸繼續進行 )
- 當邊界條件滿足時,直接求解 ( 遞歸結束 )
遞歸模型的一般表示法:
遞歸在程序設計中的應用:
- 遞歸函數
- 函數體中存在自我調用的函數
- 遞歸函數必須有遞歸出口 ( 邊界條件 )
- 函數的無限遞歸將導致程序崩潰
- 函數體中存在自我調用的函數
遞歸思想的應用:
- 求解:Sum( n ) = 1 + 2 + 3 + ... + n
遞歸求和:
unsigned int sum(unsigned int n)
{
if( n > 1 )
{
return n + sum(n-1);
}
else
{
return 1;
}
}
斐波拉契數列:
數列自身遞歸定義:1, 1, 2, 3, 5, 8, 13, 21, ...
斐波拉契數列:
unsigned int fac(unsigned int n) { if( n > 2 ) { return fac(n-1) + fac(n-2); } if( (n == 2) || (n == 1) ) { return 1; } return 0; }
用遞歸的方法編寫函數求字符串長度:
用遞歸的方法編寫函數求字符串長度:
unsigned int _strlen_(const char* s)
{
if( *s != ‘\0‘ )
{
return 1 + _strlen_(s+1);
}
else
{
return 0;
}
}
或
unsigned int _strlen_(const char* s)
{
return s ? (*s ? (1 + _strlen_(s+1)) : 0) : 0;
}
2.遞歸的應用
2.1 單向鏈表的轉置
預備的單鏈表:
#include <iostream>
using namespace std;
struct Node
{
int value;
Node* next;
};
Node* create_list(int v, int len)
{
Node* ret = NULL;
Node* slider = NULL;
for(int i=0; i<len; i++)
{
Node* n = new Node();
n->value = v++;
n->next = NULL;
if( slider == NULL )
{
slider = n;
ret = n;
}
else
{
slider->next = n;
slider = n;
}
}
return ret;
}
void destroy_list(Node* list)
{
while( list )
{
Node* del = list;
list = list->next;
delete del;
}
}
void print_list(Node* list)
{
while( list )
{
cout << list->value << "->";
list = list->next;
}
cout << "NULL" << endl;
}
int main()
{
Node* list = create_list(1, 5);
print_list(list);
destroy_list(list);
return 0;
}
運行結果為:
1->2->3->4->5->NULL
單向鏈表的轉置:
Node* reverse(Node* list)
{
if( (list == NULL) || (list->next == NULL) )
{
return list;
}
else
{
Node* guard = list->next;
Node* ret = reverse(list->next);
guard->next = list;
list->next = NULL;
return ret;
}
}
int main()
{
Node* list = create_list(1, 5);
print_list(list);
list = reverse(list);
print_list(list);
destroy_list(list);
return 0;
}
運行結果為:
1->2->3->4->5->NULL
5->4->3->2->1->NULL
2.2 單向排序鏈表的合並
單向排序鏈表的合並:
Node* merge(Node* list1, Node* list2)
{
if( list1 == NULL )
{
return list2;
}
else if( list2 == NULL )
{
return list1;
}
else if( list1->value < list2->value )
{
Node* list_1 = list1->next;
Node* list = merge(list_1, list2);
list1->next = list;
return list1;
}
else
{
Node* list_2 = list2->next;
Node* list = merge(list1, list_2);
list2->next = list;
return list2;
}
}
int main()
{
Node* list1 = create_list(1, 5);
Node* list2 = create_list(2, 6);
print_list(list1);
print_list(list2);
Node* list = merge(list1, list2);
print_list(list);
destroy_list(list);
return 0;
}
運行結果為:
1->2->3->4->5->NULL
2->3->4->5->6->7->NULL
1->2->2->3->3->4->4->5->5->6->7->NULL
代碼優化:
Node* merge(Node* list1, Node* list2)
{
if( list1 == NULL )
{
return list2;
}
else if( list2 == NULL )
{
return list1;
}
else if( list1->value < list2->value )
{
return (list1->next = merge(list1->next, list2), list1);
}
else
{
return (list2->next = merge(list1, list2->next), list2);
}
}
2.3 漢諾塔問題
漢諾塔問題:
- 將木塊借助 B 柱由 A 柱移動到 C 柱
- 每次只能移動一個木塊
- 只能出現小木塊在大木塊之上
漢諾塔問題分解:
- 將 n-1 個木塊借助 C 柱由 A 柱移動到 B 柱
- 將最底層的唯一木塊直接移動到 C 柱
- 將 n-1 個木塊借助 A 柱由 B 柱移動到 C 柱
漢諾塔問題:
void HanoiTower(int n, char a, char b, char c) // a ==> src, b ==> middle, c ==> dest
{
if( n == 1 )
{
cout << a << "-->" << c << endl;
}
else
{
HanoiTower(n-1, a, c, b);
HanoiTower(1, a, b, c);
HanoiTower(n-1, b, a, c);
}
}
int main()
{
HanoiTower(3, ‘a‘ ,‘b‘, ‘c‘);
return 0;
}
運行結果為:
a-->c
a-->b
c-->b
a-->c
b-->a
b-->c
a-->c
2.4 全排列問題
全排列問題:
void permutation(char* s, char* e) // e始終指向字符數組的首元素
{
if( *s == ‘\0‘ )
{
cout << e << endl;
}
else
{
int len = strlen(s);
for(int i=0; i<len; i++)
{
swap(s[0], s[i]);
permutation(s+1, e);
swap(s[0], s[i]);
}
}
}
int main()
{
char s[] = "abc";
permutation(s, s);
return 0;
}
運行結果為:
abc
acb
bac
bca
cba
cab
但是如果存在相同的元素,則會有重復結果,例如:
int main()
{
char s[] = "aac";
permutation(s, s);
return 0;
}
運行結果為:
aac
aca
aac
aca
caa
caa
代碼優化:
void permutation(char* s, char* e) // e始終指向字符數組的首元素
{
if( *s == ‘\0‘ )
{
cout << e << endl;
}
else
{
int len = strlen(s);
char mark[256] = {0};
for(int i=0; i<len; i++)
{
if( !mark[s[i]] )
{
swap(s[0], s[i]);
permutation(s+1, e);
swap(s[0], s[i]);
mark[s[i]] = 1;
}
}
}
}
int main()
{
char s[] = "aac";
permutation(s, s);
return 0;
}
運行結果為:
aac
aca
caa
2.5 逆序打印單鏈表中的偶數結點
遞歸還能用於需要回溯窮舉的場合。。。
函數調用過程回顧:
- 程序運行後有一個特殊的內存區供函數調用使用
- 用於保存函數中的實參,局部變量,臨時變量,等
- 從起始地址開始往一個方向增長 ( 如 : 高地址 → 低地址 )
- 有一個專用“指針”標識當前已使用內存的“頂部”
程序中的棧區:一段特殊的專用內存區
實例分析:逆序打印單鏈表中的偶數結點
逆序打印單鏈表中的偶數結點:
void r_print_even(Node* list)
{
if( list != NULL )
{
r_print_even(list->next);
if( (list->value % 2) == 0 )
{
cout << list->value << endl;
}
}
}
int main()
{
Node* list = create_list(2, 5);
print_list(list);
r_print_even(list);
destroy_list(list);
return 0;
}
運行結果為:
2->3->4->5->6->NULL
6
4
2
2.6 八皇後問題
八皇後問題:
- 在一個8x8的國際象棋棋盤上,有8個皇後,每個皇後占一格;要求皇後間不會出現相互“攻擊”的現象 ( 不能有兩個皇後處在同一行、同一列或同一對角線上 )。
關鍵數據結構定義:
- 棋盤:二維數組 ( 10 * 10 )
- 0 表示位置為空,1 表示皇後,2 表示邊界
- 位置:struct Pos;
- 方向:
算法思路:
八皇後問題:
#include <iostream>
#include "LinkList.h"
using namespace std;
using namespace StLib;
template <int SIZE>
class QueueSolution : public Object
{
protected:
enum { N = SIZE + 2 };
struct Pos : public Object
{
Pos(int px = 0, int py = 0) : x(px), y(py) { }
int x;
int y;
};
int m_chessboard[N][N];
Pos m_direction[3];
LinkList<Pos> m_solution;
int m_count;
void init()
{
m_count = 0;
for(int i=0; i<N; i+=(N-1))
{
for(int j=0; j<N; j++)
{
m_chessboard[i][j] = 2;
m_chessboard[j][i] = 2;
}
}
for(int i=1; i<=SIZE; i++)
{
for(int j=1; j<=SIZE; j++)
{
m_chessboard[i][j] = 0;
}
}
m_direction[0].x = -1;
m_direction[0].y = -1;
m_direction[1].x = 0;
m_direction[1].y = -1;
m_direction[2].x = 1;
m_direction[2].y = -1;
}
void print()
{
for(m_solution.move(0); !m_solution.end(); m_solution.next())
{
cout << "(" << m_solution.current().x << ", " << m_solution.current().y << ") ";
}
cout << endl;
for(int i=0; i<N; i++)
{
for(int j=0; j<N; j++)
{
switch (m_chessboard[i][j])
{
case 0: cout << " "; break;
case 1: cout << "#"; break;
case 2: cout << "*"; break;
}
}
cout << endl;
}
cout << endl;
}
bool check(int x, int y, int d)
{
bool flag = true;
do
{
x += m_direction[d].x;
y += m_direction[d].y;
flag = flag & (m_chessboard[x][y] == 0);
}
while( flag );
return (m_chessboard[x][y] == 2);
}
void run(int j) // 檢查第j行有沒有可以放置皇後的位置
{
if( j <= SIZE )
{
for(int i=1; i<=SIZE; i++)
{
if( check(i ,j, 0) && check(i ,j, 1) && check(i ,j, 2) )
{
m_chessboard[i][j] = 1;
m_solution.insert(Pos(i, j));
run(j + 1);
m_chessboard[i][j] = 0;
m_solution.remove(m_solution.length() - 1);
}
}
}
else
{
m_count++;
print();
}
}
public:
QueueSolution()
{
init();
}
void run()
{
run(1);
cout << "Total: " << m_count << endl;
}
};
int main()
{
QueueSolution<4> qs;
qs.run();
return 0;
}
測試四皇後問題的運行結果為:
(2, 1) (4, 2) (1, 3) (3, 4)
******
* # *
*# *
* #*
* # *
******
(3, 1) (1, 2) (4, 3) (2, 4)
******
* # *
* #*
*# *
* # *
******
Total: 2
(八皇後問題一共有92個解。)
3.小結
- 遞歸是一種將問題分而自治的思想
- 用遞歸解決問題首先要建立遞歸的模型
- 遞歸解法必須要有邊界條件,否則無解
- 不要陷入遞歸函數的執行細節,學會通過代碼描述遞歸問題
- 程序運行後的棧存儲區專供函數調用使用
- 棧存儲區用於保存實參,局部變量,臨時變量,等
- 利用棧存儲區能夠方便的實現回溯算法
- 八皇後問題是棧回溯的經典應用
數據結構開發(15):遞歸的思想與應用