感悟數據結構的實驗真的很重要,感覺實驗做的好一般都能得高分,所以大家做完正式實驗後儘量做一下補充實驗有好處。時間和精力有限,我只是整理了正式實驗的,好了上乾貨。
1.實驗一 遞歸練習
1)要求:
(我只是完善邏輯,並沒有關於輸入輸出格式的代碼。)
2)邏輯
以四個數爲例:全排列數爲24,只以一爲開頭的有6種
1.2.3.4爲第一個。
1.2.4.3
1.3.2.4
1.3.4.2
1.4.3.2
1.4.2.3
每當長度等於4時就輸出結果,遞歸地將位置交換
for (int i = start; i < end; ++i) {
swap(list[start],list[i]);
permutation(list,start+1,end);
swap(list[start],list[i]);
}
3)代碼:
//輸出方法
void system(int list[],int end){
for (int i = 0; i < end ; ++i) {
cout<<list[i];
if(i!=end-1){
cout<<",";
}
}
cout<<endl;
}
//實現方法
void permutation(int list[],int start,int end){
if(start == end){
system(list,end);
} else{
for (int i = start; i < end; ++i) {
swap(list[start],list[i]);
permutation(list,start+1,end);
swap(list[start],list[i]);
}
}
}
結果:
2.排序算法
1)要求:
使用排序方法1-Bubble Sort,2-Insert Sort,3-Radix Sort對 2-20 個不爲零的正整數進行排序。
Bubble Sort:
冒泡排序又稱爲泡式排序,是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端
Insert Sort:
插入排序是一種簡單直觀的排序算法。
Radix Sort:
基數排序是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。
2)邏輯
冒泡排序:
1.比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
2.對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
3.針對所有的元素重複以上的步驟,除了最後一個。
4.持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
插入排序:
它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。
基數排序:
將所有待比較數值(正整數)統一爲同樣的數字長度,數字較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。(利用箱子排序的穩定性!)
3)代碼
冒泡排序:
//及時終止的冒泡排序
void bubbleSort(int a[],int n){
bool sorted = false;//用於控制終止程序
for(int i=1;i<n&&!sorted;i++){//控制進行冒泡的次數,冒n-1次
sorted = true;//如果數組沒有經過一次交換說明當前數組已經處於有序狀態,則無需進行排序了
for(int j=0;j<n-i;j++){
if(a[j+1]<a[j]) {
swap(a[j],a[j+1]);
sorted = false;
}
}
}
}
插入排序:
//插入排序法
void insert(int a[],int n,const int x ){
int i = 0;
for(i=n-1;i>=0&&x<a[i];i--) a[i+1] = a[i];//凡是比x大的就右移一格!!!重要的一點數組的長度必須大於n
a[i+1] = x;
}
void insertSort(int a[],int n){
for(int i=1;i<n;i++){
int t = a[i];
insert(a,i,t);//將右邊的元素逐漸插入到左邊有序的序列中
}
}
基數排序:
這個比較難理解:參考
//基數排序
bool radixSort(int a[],int n){
for (int i = 0; i < sizeof(a)/ sizeof(int) ; ++i) {
if (a[i]>9){
cout<<"0"<<endl;
return false;
}
}
int box[10][20];//定義二維數組,並賦初值
for (int k = 0; k < 10; ++k) {
for (int i = 0; i < 20; ++i) {
box[k][i] = -1;
}
}
for (int j = 0; j < n; ++j) {
int t = 0;
while (box[a[j]][t] != -1){
t++;//相同基數的數字也能正常排序
}
box[a[j]][t] = a[j];
}
int num = 0;
for (int l = 0; l < 10; ++l) {
for (int i = 0; i < 20 ; ++i) {
if (box[l][i]!=-1){
a[num] = box[l][i];
num++;
} else{
break;
}
}
}
return true;
}
3.線性表的操作
這個比較簡單幾乎就是書上的代碼,這裏就不贅述了。
4.堆棧的應用
這個我當時做的非常之艱難,查了好多博文,但是當思路真正弄清楚後一切就豁然開朗了。
1)要求
2)邏輯
我一開始想的是把所有符號全部放入堆棧然後一個一個取出然後計算,但是總是不對,棧(stack)又名堆棧,它是一種運算受限的線性表。限定僅在表尾進行插入和刪除操作的線性表。 既然這樣那麼如果將所有符號全部壓入棧在計算就會變得異常困難,不如邊壓入棧邊計算。
我設置兩個棧一個放數字,一個放符號
數字的關鍵在於將多位數壓入棧中
符號的關鍵在於判斷優先級和括號的處理
將輸入的運算式作爲字符串,每個字符依次輸入,當其爲數字時看前一個是否爲數字,是則合爲一起壓入數字棧,否則直接壓入數字棧。當其爲符號時,對前面的符號判斷優先級,如果前面的高,則將前面的運算結果壓入數字棧,然後將符號壓入符號棧,否則直接將符號壓入符號棧。
3)代碼
在思路之外,提前將符號棧壓入一個特殊符號,利於判斷符號棧是否到頭和符號的壓入棧。
1.判斷字符是否是數字
//判斷字符是否爲數字
bool isnum(char location) {
int num_lo = location;
if (num_lo>=48&&num_lo<=57){
return true;
}
return false;
}
2.計算數字的運算結果
//用與計算結果的方法,參數是數字堆棧和運算符判斷
template <class T>
void result(char ch,linkedStack<T> &figures) //計算並將計算結果壓入棧中
{
T x,y,result;
x=figures.top();
figures.pop();
y=figures.top();
figures.pop();
switch(ch)
{
case '+':result=y+x;break;
case '-':result=y-x;break;
case '*':result=y*x;break;
case '/':result=y/x;break;
}
figures.push(result);
}
3.判斷符號的優先級
//運算符和前一個運算符的優先級比較
bool prev(char s1,char s2 ){
if(s1=='*'||s1=='/') //如果棧頂符號爲* / 則不用考慮輸入拿來比較的符號,直接返回真
return true;
else if((s1=='+'||s1=='-')&&(s2=='+'||s2=='-'))//如果棧頂符號和拿來比較的符號爲+或者-,也返回真
return true;
else
return false; //其他情況都返回假
}
4.主運算函數
int calculate(string str){
char *ch = (char*)str.c_str();//可以將string賦給char*
linkedStack<char> symbols;//存運算符
linkedStack<int> figures;//存整數,只舍不入
symbols.push('O');//添加一個結束(開始)的標記位,省去很多判斷的麻煩
char previous;//用於記錄棧頂運算符
for (int i = 0; i <str.size() ; ++i) {
char temp = ch[i];
int figure = 0;
if (isnum(temp)){
if (i==0){//如果第一個字符是數字則無序判斷前面是否也是數字
figure = (temp-'0');//char->int
figures.push(figure);
} else{
if (isnum(ch[i-1])){
figures.top() = figures.top()*10+(temp-'0');
} else{//前面不是數字則將自己壓入棧中
figure = (temp-'0');
figures.push(figure);
}
}
} else if (temp=='('){
symbols.push(temp);
} else if (temp==')'){//右括號不如符號棧
previous = symbols.top();
while (previous!='('){
result(previous,figures);
symbols.pop();
previous = symbols.top();
}
symbols.pop();//將( 彈出棧
}
//是運算符
else{
previous = symbols.top();
while (prev(previous,temp)){
result(previous,figures);
symbols.pop();//將已經運算過的運算符彈出棧
previous = symbols.top();//再比較更前面的
}
symbols.push(temp);//將未運算的符號壓入棧中
}
}
previous = symbols.top();
while (previous!='O'){//只要還有運算符計算就沒結束
result(previous,figures);
symbols.pop();
previous=symbols.top();
}
return figures.top();
}
棧的數據結構實現形式可以是數組可以是鏈表,個人傾向於鏈表。將頭結點作爲棧頂十分便捷,插入和刪除操作都集中在頭結點。
插入:
void push(const T& theElement){//壓棧時無需判斷棧是否爲空
stackTop = new chainNode<T>(theElement,stackTop);//很巧妙!
stackSize++;
}
刪除:
void pop(){
if (stackSize == 0) throw;
//stackTop = stackTop->next;錯誤刪除方法,只是改變頭結點位置並沒有真正刪除第一個元素。
chainNode<T> *temp = stackTop->next;
delete stackTop;
stackTop = temp;
stackSize--;
}
5.二叉樹操作
1)要求
二叉樹(英語:Binary tree)是每個節點最多隻有兩個分支(即不存在分支度大於2的節點)的樹結構。通常分支被稱作“左子樹”或“右子樹”。二叉樹的分支具有左右次序,不能隨意顛倒。
二叉樹是本課程最爲重要的部分之一,可考的很多,遍歷,節點數目,高度,葉子節點的數目……
1、輸入一個完全二叉樹的層次遍歷字符串,創建這個二叉樹,輸出這個 二叉樹的前序遍歷字符串、中序遍歷字符串、後序遍歷字符串、結點 數目、二叉樹高度(上述每一個結果獨立一行顯示)。
2、輸入二叉樹前序序列和中序序列(各元素各不相同),創建這個二叉 樹,輸出該二叉樹的後序序列、層次遍歷。
2)邏輯
1、由完全二叉樹的層次遍歷創建二叉樹比較簡單。利用隊列將元素逐漸放到完全二叉樹上。
2、由前序和中序序列創建二叉樹,比較難
根據前序判斷根節點,根據中序和根節點的位置確定左右子樹一直這樣逐漸形成一棵樹。
3)代碼
1.由層次遍歷創建完全二叉樹
template<class T>
//按層次遍歷的順序創建樹
void linkedBinaryTree<T>::LevelCreateTree(int nodeSize,char* string) {
treeSize = nodeSize;
//層次遍歷有關的都要結合隊列來做
arrayQueue<binaryTreeNode<T>*> *queues = new arrayQueue<binaryTreeNode<T>*>;
int count = 1;
binaryTreeNode<T> *node = new binaryTreeNode<T>();//用來移動的節點Node
root = new binaryTreeNode<T>();
node = root;
root->element = string[count-1];//頭結點
//只有一個元素時
if (count == nodeSize){
return;
}
queues->push(node);//將頭節點放入隊列中
while (!queues->empty()) {
node = queues->front();
queues->pop();
if (node->leftChild == NULL) {
binaryTreeNode<T> *left = new binaryTreeNode<T>();
left->element = string[count];
node->leftChild = left;
queues->push(left);
count++;
if (count == nodeSize)//這時候中止並不會有數組越界的bug
break;
}
if (node->rightChild == NULL) {
binaryTreeNode<T> *right = new binaryTreeNode<T>;
right->element = string[count];
node->rightChild = right;
queues->push(right);
count++;
if (count == nodeSize)
break;
}
}
}
2.樹的高度
template <class T>
//計算樹的高度
int linkedBinaryTree<T>::height_tree(binaryTreeNode<T>* tree){
if(tree==NULL) return 0;
else return 1+max(height_tree(tree->leftChild),height_tree(tree->rightChild));//!!!!不要忘記1是父節點
}
3.樹的節點數目
template <class T>
//計算樹的結點個數
int linkedBinaryTree<T>::getNodesNum(binaryTreeNode<T> *root) {
if (root==NULL) return 0;
else return 1+getNodesNum(root->leftChild)+getNodesNum(root->rightChild);
}
4.由前序和中序遍歷創建二叉樹
//利用前序遍歷和中序遍歷創建二叉樹
template <class T>
binaryTreeNode<T>* linkedBinaryTree<T>::CreateByPreIn(T *VLR, T *LVR, int n) {//n是前序或者中序結果的長度
if (n == 0)
return NULL;
int k = 0;
while (VLR[0] != LVR[k])
k++;
binaryTreeNode<T> *t = new binaryTreeNode<T>();
t->element = VLR[0];//父節點創建
t->leftChild = CreateByPreIn(VLR + 1, LVR, k);
t->rightChild = CreateByPreIn(VLR + k + 1, LVR + k + 1, n - k - 1);
return t;
}
6.堆和搜索樹
1)要求
1、輸入一系列不爲零的正整數(最多不超過 20 個),遇到 0 代表輸入 結束(不包含 0)。
2、根據上面輸入的數據序列,用初始化方法創建最大堆(不要用節點依 次插入的辦法創建最大堆),然後輸出最大堆的層次序列。
3、輸出用堆排序後的排序結果。
4、根據上面輸入的數據,創建二叉搜索樹(關鍵字不允許重複,如遇重 復,則不重複插入該關鍵字),輸出二叉搜索樹的前序序列、中序序 列(分行輸出)。
二叉查找樹(英語:Binary Search Tree),也稱爲二叉搜索樹、有序二叉樹(ordered binary tree)或排序二叉樹(sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:
1.若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
2.若任意節點的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
3.任意節點的左、右子樹也分別爲二叉查找樹;
4.沒有鍵值相等的節點。
2)邏輯
1.堆的初始化:
最大堆一定爲完全二叉樹所以用數組表示最爲方便。
以最小堆的初始化爲例
爲了將圖一的完全二叉樹轉變爲最小堆,從最後一個具有孩子的節點(97)開始將孩子中最小的與自己比較,如果比孩子大則交換(如果以這個節點爲根的節點不是最小堆則將其轉換爲最小堆。)一直轉換到根。
2.堆排序
將堆初始化然後依次刪除根節點即可得到有序的數列。
3.二叉搜索樹
將元素依次插入到二叉搜索樹中:
3)代碼
1.堆的初始化:
//堆的初始化,時間複雜度爲Θ(n),比一個一個的插入性能要好一些
template <class T>
void maxHeap<T>::initialize(T *theHeap, int theSize) {
delete [] heap;
heap = theHeap;
heapSize = theSize;
//從第一個有孩子的節點開始排查
for (int root = heapSize/2; root >=1 ; --root) {
T theElement = heap[root];
int child = 2*root;
while (child<=heapSize){
if (child<heapSize&&heap[child]<heap[child+1]){
child++;//獲取大孩子
}
if (theElement>=heap[child]){
break;
}
//父子交換
heap[child/2] = heap[child];
child *= 2;
}
heap[child/2] = theElement;
}
}
2.堆排序:
template <class T>
T* sort2(T a[],int n){
maxHeap<T> heap(1);
heap.initialize(a,n);
for (int i = n-1; i >=1 ; --i) {
T x = heap.top();
heap.pop();
a[i+1] = x;
}
return a;
}
3.二叉樹的插入:
template <class K>
void binarySearchTree<K>::insert(int & thePair) {
// cout<<thePair<<endl;檢驗代碼
binaryTreeNode<int> *pp = NULL;
binaryTreeNode<int> *p = this->root;//問題:c++模板類在繼承中子類無法訪問父類的成員 解決方法:在子類訪問父類時加上父類的前綴或使用this->調用
while (p!=NULL){
pp = p;//記錄p的位置
if(thePair<p->element){//要插入的元素比當前元素小,往左子樹走
p = p->leftChild;
} else{
if (thePair>p->element){
p = p->rightChild;
} else{
return;//不重複插入元素,並不覆蓋
}
}
}
binaryTreeNode<int> *newNode = new binaryTreeNode<int>(thePair);
if (this->root!=NULL){
if (pp->element>thePair){
pp->leftChild = newNode;
} else{//不存在=的情況
pp->rightChild = newNode;
}
} else{
this->root = newNode;
}
this->treeSize++;
}
7.圖的操作
1)要求
1.輸出從第 1 節點到第 n 節點最短路徑的長度,如果沒有路經,輸出 0。(單源最短路徑)
2.輸出最小生成樹的所有邊。
2)邏輯
1.使用迪傑斯特拉算法
2.使用最小生成樹算法
迪傑斯特拉:(僞碼)(提示考試讓你寫代碼,如果真的寫不出C++代碼建議寫僞碼)
最小生成樹算法
有三種算法:
Kruskal算法,Prim算法,Sollin算法。(重點掌握前兩種)
1.Kruskal算法
從剩下的邊集中選出一條成本最小的且不會與已加入的邊產生環路
難點如何判讀加入邊後不會產生環路,使用等價類。
2.Prim算法
從剩餘的邊中選出一條成本最小的邊並能加入已選入的邊集中形成樹。
3)代碼:
迪傑斯特拉算法:
//3.迪傑斯特拉算法
void Dijkstra(int n, int v){
int maxnum = 100;
int **c = a;
int dist[n+1];
int prev[n+1];
bool s[maxnum]; // 判斷是否已存入該點到S集合中
for(int i=1; i<=n; ++i)
{
dist[i] = c[v][i];
s[i] = 0; // 初始都未用過該點
if(dist[i] == noEdge)
prev[i] = 0;
else
prev[i] = v;
}
dist[v] = 0;
s[v] = 1;
// 依次將未放入S集合的結點中,取dist[]最小值的結點,放入結合S中
// 一旦S包含了所有V中頂點,dist就記錄了從源點到所有其他頂點之間的最短路徑長度
// 注意是從第二個節點開始,第一個爲源點
for(int i=2; i<=n; ++i){
int tmp = noEdge;
int u = v;
// 找出當前未使用的點j的dist[j]最小值
for(int j=1; j<=n; ++j)
if((!s[j]) && dist[j]<tmp)
{
u = j; // u保存當前鄰接點中距離最小的點的號碼
tmp = dist[j];
}
s[u] = 1; // 表示u點已存入S集合中
// 更新dist
for(int j=1; j<=n; ++j)
if((!s[j]) && c[u][j]<noEdge)
{
int newdist = dist[u] + c[u][j];
if(newdist < dist[j])
{
dist[j] = newdist;
prev[j] = u;
}
}
}
if (dist[n]==noEdge){
cout<<0;
} else{
cout<<dist[n];
}
}
Kruskal算法
返回值是bool主要是如果沒有生成樹則返回false
//4.Ktuskal算法 計算最小生成樹
bool kruskal(Edges spanning[]){
int n = this->n;
int e = this->e;
//邊集合E
Edges edges[e];
//對邊集合E初始化
int k = 0;
for (int i = 1; i <= n ; ++i) {
for (int j = 1; j <= n ; ++j) {
if(existsEdge(i,j)&&i<j){
edges[k].start = i;
edges[k].end = j;
edges[k].weight = a[i][j];
k++;
}
}
}
//並查集問題!
fastUnionFind uf(n+1);
k =0;
while (e>0&&k<n-1){
Edges x = min_from(edges,this->numberOfEdges());
e--;
int a = x.start;
int b = x.end;
if(uf.find(a) != uf.find(b)){//!!!!!!!!!!!!!!!!!!!判斷新加入的邊是否成環
//加入生成樹中
spanning[k++] = x;
uf.unite(a,b);
}
}
return (k == n-1);
}
並查集:也是這門課貫穿始終的問題很重要,也很常用參考博文
class fastUnionFind{
private:
int n;//節點數目
int *node; //每個節點
int *rank; //樹的高度
public:
//初始化n個節點
void Init(int n){
for(int i = 0; i < n; i++){
node[i] = i;
rank[i] = 0;
}
}
//構造函數
fastUnionFind(int n) : n(n) {
node = new int[n];
rank = new int[n];
Init(n);
}
//析構函數
~fastUnionFind(){
delete [] node;
delete [] rank;
}
//查找當前元素所在樹的根節點(代表元素)
int find(int x){
if(x == node[x])
return x;
return node[x] = find(node[x]); //在第一次查找時,將節點直連到根節點
}
//合併元素x, y所處的集合
void unite(int x, int y){
//查找到x,y的根節點
x = find(x);
y = find(y);
if(x == y)
return ;
//判斷兩棵樹的高度,然後在決定誰爲子樹
if(rank[x] < rank[y]){
node[x] = y;
}else{
node[y] = x;
if(rank[x] == rank[y]) rank[x]++;
}
}
//判斷x,y是屬於同一個集合
bool same(int x, int y){
return find(x) == find(y);
}
};
Prim算法
//最小生成樹的prim算法
bool Prim(Edges spanning[]){
int n = this->n;
int e = this->e;
//邊集合E
Edges edges[e];
//對邊集合E初始化
int k = 0;
for (int i = 1; i <= n ; ++i) {
for (int j = 1; j <= n ; ++j) {
if(existsEdge(i,j)&&i<j){
edges[k].start = i;
edges[k].end = j;
edges[k].weight = a[i][j];
k++;
}
}
}
/*
* 鏈表儲存已經加入到TV中的點
*/
chain<int> theVs;
k =0;
theVs.insert(0,1);//起始點是1
while (e>0&&k<n-1){
Edges x = min_from_vs(theVs,edges,this->numberOfEdges());
e--;
spanning[k++] = x;
/*
* 將另一個頂點加入到點集中
*/
if (theVs.indexOf(x.start)==0){
theVs.insert(0,x.start);
} else{
theVs.insert(0,x.end);
}
}
return (k == n-1);
}
關鍵是判斷最小的邊加入後是否還是一棵樹:
/*
* prim的輔助方法
*/
Edges min_from_vs(chain<int> vs,Edges es[],int bianshu){
Edges temp = es[0];
int index = 0;
int sum = 0;
while(true){
for (int i = 1; i < bianshu-sum ; ++i) {
if (es[i].weight<temp.weight){
temp = es[i];
index = i;
}
}
/*
* 一個頂點在vs中一個頂點不在vs中
*/
if ((vs.indexOf(temp.start)!=0&&vs.indexOf(temp.end)==0)||(vs.indexOf(temp.end)!=0&&vs.indexOf(temp.start)==0)){
es[index].weight = 10000;//相當於刪除最小的邊
return temp;
} else{
swap(es[index],es[e-1-sum]);
sum++;
}
}
}