資料結構與演算法碎片積累(二)

鴻_H發表於2020-11-24

背景:非科班梳理基礎知識ing

前言:資料結構與演算法碎片積累(一)有60個小題(20節課),感覺看起來太多了,所以,接下來根據課程安排,採用課程內容分類方式來總結。這樣子,或許回頭看時候,更人性化一些。

20201124
1、啥叫雙向連結串列?
答:簡單理解,就是一個節點,由資料域data,有前指標prior,指標next組成。

typedef char ElemType;
typedef struct DualNode {
	ElemType data;
	struct DualNode* prior;//前驅結點
	struct DualNode* next;//後繼結點
};

在這裡插入圖片描述
2、雙向連結串列插入和刪除操作是怎麼樣的?
答:
1)插入:
在這裡插入圖片描述
假設節點s插到節點p的前方,核心語句:

s->next=p;//s的後指標指向p
s->prior=p->prior;//s的前指標指向p的前一個結點
p->prior->next=s;//p的前一個結點的後指標指向s
p->prior=s;//p的前指標指向s

插入操作時候,一定要注意指標的指向以及先後順序

2)刪除:
在這裡插入圖片描述

假設節點p為待刪除節點,關鍵語句:

   p->prior->next=p->next;
   p->next->prior=p->prior;
   free(p);//釋放刪除節點所佔的記憶體

關鍵就是把節點p的前節點的後指標指向p的後節點;p的後節點的前指標指向p的前結點,釋放節點p內容。

3、雙向連結串列相對於單連結串列來說,有啥特色?
答:
1)雙向連結串列相對單連結串列而言,更復雜一些,每個節點多一個prior指標,對於插入和刪除操作的順序一定要注意;
2)雙向連結串列可以有效提升演算法的時間效能,也就是利用空間換取時間。
3)簡單理解,雙向連結串列有前後指向指標,指向更加靈活。

4、雙向連結串列的應用例項演示。
答:
–要求實現使用者輸入一個數使得26個字母的排序發生變化,例如使用者輸入3,輸出結果:
DEFGHIJKLMNOPQRSTUVWXYZABC
–同時需要支援負數,例如使用者輸入-3,輸出結果:
XYZABCDEFGHIJKLMNOPQRSTUVW

//26字母輸出,練習雙向連結串列
#include<stdio.h>
#include<stdlib.h>

#define OK 1
#define ERROR 0

typedef char ElemType;
typedef int Status;

//雙向連結串列
typedef struct DualNode {
	ElemType data;
	struct DualNode* prior;//前驅結點
	struct DualNode* next;//後繼結點
}DualNode, * DuLinkList;

//初始化一個雙向連結串列,以及插入資料到雙向連結串列中
Status InitList(DuLinkList* L)
{
	DualNode* p, * q;//節點指標
	int i;

	*L = (DuLinkList)malloc(sizeof(DualNode));
	if (!(*L))
	{
		return ERROR;
	}//開闢失敗,退出函式

	(*L)->next = (*L)->prior = NULL;
	p = *L;

	for (i = 0; i < 26; i++)//依次將26個字母插入到連結串列中
	{
		q = (DualNode*)malloc(sizeof(DualNode));//開闢新結點

		if (!q)
		{
			return ERROR;//開闢失敗,退出
		}

		q->data = 'A' + i;//這種方式儲存26個字母到雙向連結串列裡面

		
		q->prior = p;
		q->next = p->next;//注意,最開始的p->next都是NULL的,這一步
						  //保持了連結串列的最後一個結點的後指標都是指向NULL
						  //所以,這一步一定要注意,順序不能和下一步調換
		p->next = q;
		p = q;//移動p到連結串列的最後面

		if (i == 25)//下面註釋步驟作用和這裡是一樣的
		{
			p->next = (*L)->next;
			(*L)->next->prior = p;
		}//這一步實現雙向迴圈連結串列(注意,這個迴圈雙向連結串列,不連線頭結點*L的)
		
	}
	//p->next = (*L)->next;
	//(*L)->next->prior = p;//實現最後結點和字元A結點連結(注意不是頭結點*L)

	return OK;
}

//這函式作用是控制表頭*L指標的移動步數,從而達到輸出字母的順序要求
void Caesar(DuLinkList* L, int i)
{
	if (i > 0)
	{
		do {
			(*L) = (*L)->next;//這裡決定了後移
		} while (--i);//由於i大於0,所以要減少來控制後移步數
	}

	if (i < 0)
	{
		do {
			(*L) = (*L)->prior;//這裡決定了前移
		} while (++i);//由於i小於0,所以要增加來控制後移步數
	}
}


int main()
{
	DuLinkList L;
	int i,n;

	InitList(&L);

	printf("請輸入一個整數:");
	scanf_s("%d", &n);//這裡由於在vs上面執行的,提示了scanf不安全
					  //需要寫成scanf_s型別

	Caesar(&L, n);

	for (i = 0; i < 26; i++)
	{
		L = L->next;

		printf("%c", L->data);
	}
	return 0;
}

5、棧是啥?
答:可以這麼理解:
1)棧是一種線性表;
2)棧是一種先進後出(如子彈入彈夾)的資料結構;
3)棧的刪除或插入操作只是在表尾(又稱棧頂top)進行;
4)棧的表尾,又稱棧頂(top);表頭,又稱為棧底(bottom)。

5)棧的插入操作(push),稱為進棧,也稱為壓棧;刪除操作(pop),叫出棧,又稱為彈棧;
6)棧有兩種儲存方式,順序儲存結構和鏈式儲存結構。

6、棧的常規操作,結構定義,入棧,出棧,清空棧,銷燬棧是哈?
答:案例展示,將一個二進位制數轉為十進位制數:

//利用棧的資料結構特點,將二進位制轉換為十進位制數
#include<stdio.h>
#include<stdlib.h>
#include<math.h>

#define STACK_INIT_SIZE 20
#define STACKINCREMENT  10

//棧結構定義
typedef char ElemType;
typedef struct {
	ElemType* top;//棧頂
	ElemType* base;//棧底
	int stackSize;//棧空間大小
}sqStack;

void InitStack(sqStack* s) {
	s->base = (ElemType*)malloc(STACK_INIT_SIZE * sizeof(ElemType));//開闢空間
	if (!s->base)
		return;//開闢失敗則退出函式

	s->top = s->base;//剛開始棧頂棧底指向相同
	s->stackSize = STACK_INIT_SIZE;//初始化棧空間大小
}//初始化一個棧

//入棧操作
void Push(sqStack* s, ElemType e)
{
	if (s->top - s->base >= s->stackSize)//檢測棧是否已經滿了
	{
		s->base = (ElemType*)realloc(s->base, (s->stackSize + STACKINCREMENT)*sizeof(ElemType));
		//複製原來的資料,並開闢到更大的記憶體中
		if (!s->base)
			return;//開闢失敗的話,退出函式

		s->stackSize = s->stackSize + STACKINCREMENT;//棧容量不跟新,會導致程式崩潰(qt執行倒不會)
	}
	*(s->top) = e;//插入元素入棧
	s->top++;//棧頂上移
}

//出棧操作
void Pop(sqStack* s, ElemType* e)
{
	if (s->top == s->base)
		return;//判斷是否出現下溢

	*e = *--(s->top);//先棧頂元素下移,然後,返回出棧元素
}

//清空棧操作
void ClearStack(sqStack* s)
{
	s->top = s->base;
	
}//將棧頂指標指向棧底,就不再讀取棧資料,達到清空目的(清空不表示資料刪除;銷燬才是將資料徹底銷燬)

//銷燬一個棧
void DestoryStack(sqStack* s)
{
	int i, len;

	len = s->stackSize;

	for (i = 0; i < len; i++)
	{
		//ElemType* p;
		//p = s->base;
		//s->base++;
		//free(p);
		free(s->base);
		s->base++;
	}//這裡存在一個bug,還沒有解決20201124
	s->base = s->top = NULL;
	s->stackSize = 0;
}//從棧底不斷上移,並釋放記憶體

int StackLen(sqStack s)//對比上面的可以發現,對應需要修改的,傳指標;
					   //不需要修改資料的,只是傳值
{
	return(s.top - s.base);
}

int main()
{
	ElemType c;
	sqStack s;
	InitStack(&s);//初始化棧

	int len, i, sum = 0;

	printf("請輸入二進位制數,輸入#符號表示結束!\n");

	scanf_s("%c", &c);

	while (c != '#')
	{
		Push(&s, c);
		scanf_s("%c", &c);//這裡的c是下一個入棧的字元
		//printf("%c\n", c);
	}
	getchar();//Enter鍵,產生的字元'\n'(==10),該函式可避免
			  //該字元一直在緩衝區(下次輸入操作時候,10會造成程式混亂)
			  //其作用就是將'\n'從緩衝區去掉
	len = StackLen(s);

	printf("棧的當前容量是:%d\n", len);

	for (i = 0; i < len; i++)
	{
		Pop(&s,&c);
		sum = sum+(c - 48) * pow(2, i);//pow就是被呼叫的數學次方函式
	}

	printf("轉換為十進位制數是:%d\n", sum);
	ClearStack(&s);
	int k = StackLen(s);
	if (k == 0) {
		printf("\n清空成功!\n");
	}
	//DestoryStack(&s);//銷燬棧還有bug,後面修正了再更新,銷燬思路應該沒問題的

	return 0;
}

7、棧的鏈式儲存結構插入和刪除的操作注意點?
答:
棧因為只是棧頂用來做插入和刪除操作,所以比較好的方法就是將棧頂放在單連結串列的頭部,棧頂指標和單連結串列指標合二為一。棧的鏈式儲存結構圖示可如:
在這裡插入圖片描述

typedef struct StackNode {
	ElemType data;//存放棧的資料
	struct StackNode* next;

}StackNode,*LinkStackPtr;//棧節點,鏈棧節點指標

typedef struct LinkStack {
	LinkStackPtr top;//top指標
	int count;   //棧元素計算器
};

//進棧操作
//對於棧鏈的Push操作,假設元素值為e的新節點是s,top為棧頂指標,那麼
Status Push(LinkStack* s, ElemType e)
{
	LinkStackPtr p = (LinkStackPtr)malloc(sizeof(StackNode));//開闢棧節點
	p->data = e;//儲存需要插入的元素
	p->next = s->top;//把棧頂指標儲存
	s->top = p;//把棧點放到原來棧頂位置,同時棧頂指標上移
	s->count++;//棧的容量加1
	return OK;
}
//出棧操作
Status Pop(LinkStack* s, ElemType* e) {
	LinkStackPtr p;

	if (StackEmpty(*s))//StackEmpty判斷是否為空棧,空的話返回1
		return ERROR;

	*e = s->top->data;//儲存刪除的元素
	p = s->top;//記錄棧頂地址

	s->top = s->top->next;//棧頂指標下移
	free(p);//釋放原來棧頂記憶體
	s->count--;//棧的容量減1
	return OK;
}

8、鏈式棧的應用,逆波蘭計算器怎麼實現?
答:
1)正常表示式->逆波蘭表示式的感性認識:
a+b - > a b +
a+(b-c) -> a b c - +
a+(b-c)d -> a b c – d * +
a+d
(b-c) -> a d b c - * +
2)程式碼實現,就是通過逆波蘭表示式來計算結果:

//逆波蘭計算器
#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>

#define STACK_INIT_SIZE 20
#define STACKINCREMENT 10
#define MAXBUFFER 10

typedef double Elemtype;
typedef struct {
	Elemtype* base;
	Elemtype* top;
	int stackSize;
}aqStack;

void InitStack(aqStack *s) {
	s->base = (Elemtype*)malloc(STACK_INIT_SIZE * sizeof(Elemtype));
	if (!s->base)
		return;

	s->top = s->base;
	s->stackSize = STACK_INIT_SIZE;
}

void Push(aqStack* s, Elemtype e)
{
	//棧滿,追加空間
	if (s->top - s->base >= s->stackSize) {
		s->base = (Elemtype*)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(Elemtype));
		if (!s->base)
			return;

		s->top = s->base + s->stackSize;
		s->stackSize = s->stackSize + STACKINCREMENT;
	}
	*(s->top) = e;
	s->top++;
}

void Pop(aqStack* s, Elemtype* e)
{
	if (s->top == s->base)
		return;

	*e = *--(s->top);//將棧頂元素彈出並修改棧頂指標
}//出棧

int StackLen(aqStack s)
{
	return(s.top - s.base);
}

int main() {
	aqStack s;//例項化一個棧物件
	char c;//儲存輸入的字元
	double d, e;//用於接收出棧入棧元素
	char str[MAXBUFFER];//小數的緩衝器
	int i=0;

	InitStack(&s);//初始化棧

	printf("請按逆波蘭表示式輸入待計算資料,資料間、資料與運算子間,運算子間之間用空格隔開,以#號作為結束標誌:\n");
	scanf_s("%c", &c);

	while (c!='#') {//這裡注意的是,這裡的c還是一個一個字元讀取進來的
		
		while (isdigit(c)||c=='.') {//用於過濾數字
			//該while迴圈,在不碰到空格前,會繼續遍歷c下一個字元
			//isdigit()屬於ctype庫函式中,檢測是十進位制數字符(0~9)返回非零,否則返回0
			str[i++] = c;//儲存載入進來的字元
			str[i] = '\0';//'\0'在字串中表示結束意思,必須加上這個,否則除錯會錯誤
						  //通過測試發現,如果不加這個結束符,會導致上一個字元段的保
						  //存的字元會繼續存在並使用,這裡原因是,i=0的作用是重新從陣列的
						  //第一個開始重新賦值儲存,所以每次都要新增結束符,保證不會讀取到
						  //原來遺留在陣列中的資料

			if (i >= 10) {
				printf("出錯:輸入單個資料過大!\n");
				return -1;
			}
			scanf_s("%c", &c);//驅使繼續讀取c下一個字元

			if (c == ' ')//如果碰到空格
			{
				d = atof(str);//atof()函式是將儲存在str陣列裡面的字串轉換為浮點型資料
				Push(&s, d);//轉化的浮點數入棧
				i = 0;//為下一次c字元組成的串轉換為浮點數做準備

				printf("%f\n", d);//測試獲取得到的浮點數
				break;//第一個引數入棧成功就跳出該迴圈,進入下一個字元c的讀取
			}
		}
		switch (c) {
		case '+':
			Pop(&s,&e);
			Pop(&s, &d);
			Push(&s, d + e);//為啥這裡可以使用d,e呢?很簡單,因為,有了出棧帶來的引數
			break;
		case'-':
			Pop(&s, &e);
			Pop(&s, &d);
			Push(&s, d - e);
			break;
		case'*':
			Pop(&s, &e);
			Pop(&s, &d);
			Push(&s, d * e);
			break;
		case'/':
			Pop(&s, &e);
			Pop(&s, &d);
			if (e != 0) {
				Push(&s, d / e);
			}
			else {
				printf("\n出錯:除數為零!");
				return -1;
			}
			break;
		}
		scanf_s("%c", &c);//驅使繼續讀取c下一個字元,而且在上一個內在的while迴圈內讀取過的c字元是不會再重複讀取的,
						  //而是繼續讀取,這裡感覺好像使得c在迴圈裡面成為全域性變數,遍歷過的就接著下一個遍歷
	}
	Pop(&s,&d);
	printf("\n最終的計算結果為:%f\n", d);

	return 0;
}

9、如何實現從中綴表示式轉換為字尾表示式?
答:
1)感性認識,中綴表示式->字尾表示式
(1-2)*(4+5)->1 2 – 4 5 + *

2)逆波蘭表示式(RPN),是不需要括號的字尾表示式

3)相對於中綴表示式(人比較容易理解),字尾表示式更適合計算機,因為,字尾運算時候,計算機不需不斷判斷。

4)中綴轉字尾程式碼實現:

//將中綴表示式轉為字尾表示式
#include<stdio.h>
#include<stdlib.h>
#include<math.h>

#define STACK_INIT_SIZE 20
#define STACKINCREMENT  10

typedef char ElemType;
typedef struct {
	ElemType* top;
	ElemType* base;
	int stackSize;
}sqStack;//棧

void InitStack(sqStack* s) {
	s->base = (ElemType*)malloc(STACK_INIT_SIZE * sizeof(ElemType));
	if (!s->base)
		return;

	s->top = s->base;
	s->stackSize = STACK_INIT_SIZE;
}//初始化一個棧

void Push(sqStack* s, ElemType e)
{
	if (s->top - s->base >= s->stackSize)//檢測棧是否已經滿了
	{
		s->base = (ElemType*)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
		//複製原來的資料,並開闢到更到的記憶體中
		if (!s->base)
			return;
		s->stackSize = s->stackSize + STACKINCREMENT;//棧容量不跟新,會導致程式崩潰(qt執行倒不會)
	}
	*(s->top) = e;
	s->top++;
}//入棧操作

void Pop(sqStack* s, ElemType* e)
{
	if (s->top == s->base)
		return;//判斷是否出現下溢

	*e = *--(s->top);
}//出棧操作

int StackLen(sqStack s)//對比上面的可以發現,對應需要修改的,傳指標;
					   //不需要修改資料的,只是傳值
{
	return(s.top - s.base);
}

int main() {
	sqStack s;
	char c, e;

	InitStack(&s);

	printf("請輸入中綴表示式,以#作為結束標誌:");
	scanf_s("%c", &c);

	while (c!='#')
	{
		while(c >= '0' && c <= '9')
		{
			printf("%c", c);
			scanf_s("%c", &c);//這裡使用這個,可以會出現有#情況,但是,還有
							  //沒有回到最外面的while判斷#,所以需要在下面的
							  //輸入型別中新增#判斷,如果有,那麼跳出迴圈
			if (c < '0' || c>'9') {
				printf(" ");
			}//這樣處理是為了,大於9的數值輸入的,如12,顯示時候,不分開
		}
		//數字判斷,如果是數字就直接列印

		if(']'==c)//這裡和  c==']'  意義一樣的,不過,這裡寫少一個= ,
					   //使得變成了賦值語句,編譯器是檢測不出錯誤的;而寫成
					   //變數在右側,少寫=會提醒報錯的。(常量在左側,賦值
					   //操作會報錯)
		{
			Pop(&s, &e);//出棧
			while ('['!=e)
			{
				printf("%c ", e);//列印出棧元素
				Pop(&s, &e);//繼續出棧出棧
			}//如果沒有遇到左括號,一直列印
		}
		//當輸入元素為右括號時候,需要把括號中運算子號出棧,並且把裡面+、-、*、/  列印出來
		//這模組的意思,就是把括號優先順序提升最高,把裡面的運算子列印出來,但是括號只是出棧,不用列印

		else if ('+'==c||c=='-')
		{
			if (!StackLen(s))//如果棧為空
			{
				Push(&s, c);//入棧
			}//如果棧為空,把符號元素儲存到棧中

			else {
				do {
					Pop(&s, &e);//出棧,檢測棧頂元素
					if ('[' == e) {
						Push(&s, e);
					}//如果是左括號,則左括號回棧
					else
					{
						printf("%c ",e);
					}//出棧的元素不是左括號,則列印
				} while (StackLen(s)&&e!='[');//棧不為空並且出棧元素不為左括號都要進行
				Push(&s, c);
			}//棧不為空時,輸入的+或者-則跟棧頂元素比較,沒遇到左括號,都要出棧列印;
			 //遇到左括號,就把運算子入棧
		}
		//這模組就是判斷輸入+或者-時,分兩種情況,首先判斷棧是否為空,不為空則入棧;棧不為空,那麼輸入元素
		//需要和棧頂元素比較,這時候,也分兩種情況,如果此時棧頂元素是左括號,那麼,就把元素入棧;否則,就
		//一直列印出棧
		//這裡,相當於把之前入棧的運算子+、-(不包含括號內的,上一模組已列印完畢),全部列印出棧

		else if (c == '*' || c == '/' || c == '[') {
			Push(&s, c);
		}
		//如果是*、/、[ 時候,那麼就把這幾個符號入棧

		else if (c == '#') {
			break;
		}//這裡判斷第一個while有可能出現#情況,然後跳出迴圈
		//當進行多數字(如,1000,就是4個字元組成的)判斷時候,存在一個內迴圈,如果此時已經碰到了#,不加處理
		//,下面又有一個scanf_s(繼續讀取下一個),那麼就會使得讀取到一些未知區域,會導致出錯。
	

		else
		{
			printf("\n輸入錯誤!\n");
			return -1;
		}
		//如果輸入的是其他資訊,那麼就提示輸入錯誤

		scanf_s("%c", &c);//繼續讀取下一個字元
	}//當未讀取到#符號時候,就一直迴圈判斷,看到底是列印還是入棧,還是出棧

	while (StackLen(s))
	{
		Pop(&s, &e);
		printf("%c ", e);
	}//當上面的大迴圈結束了,棧中還保留運算子的話,那麼依次從棧頂出棧並列印
	return 0;
}
//注意,原來採用的是()括號的,後面發現,除錯視窗自動為中文輸入法,導致
//容易即使輸入方式為中文輸入也看不出來,所以,採用[]代替()
//雖然,通過分步除錯理清楚了每一步的作用,但是,內在的演算法思路還是沒有很清晰的層次樣式

10、佇列有啥特性?
答:
1)佇列,只能一端插入,另一端刪除的線性表;
2)佇列,是一種先進先出的線性表(注意與先進後出的棧區分)

3)佇列,可用順序表實現,也可以用連結串列實現;但,一般情況下,使用連結串列來實現,所以,又可簡稱為鏈佇列。
4)佇列的結構特性:
在這裡插入圖片描述
空佇列時,front和rear都指向頭結點
在這裡插入圖片描述
11、佇列全家桶(結構、入隊、出隊、銷燬操作)是啥?
答:案例展示:

#include <malloc.h>
#include<stdio.h>
#include <stdlib.h>

//鏈佇列的結構程式碼
#define ElemType char
typedef struct QNode {
	ElemType data;
	struct QNode* next;
}QNode,*QueuePrt;//節點結構

typedef struct 
{
	QueuePrt front, rear;//隊頭、隊尾指標
}LinkQueue;

//建立或者初始化一個佇列
void initQueue(LinkQueue* q) {
	q->front = q->rear = (QueuePrt)malloc(sizeof(QNode));
	if (!q->front)
		exit(0);//這是一個函式,而return是一個關鍵字
	q->front->next = NULL;
}

//入隊操作
void InsertQueue(LinkQueue* q, ElemType e) {
	QueuePrt p;
	p = (QueuePrt)malloc(sizeof(QNode));
	if (p == NULL)
		exit(0);
	p->data=e;
	p->next = NULL;
	q->rear->next = p;//q的隊尾由原來指向null,重新指向新加入的p
	q->rear = p;//將佇列q的隊尾指標重新指向最後一個節點
}

//出隊操作
void DeleteQueue(LinkQueue* q, ElemType* e) {
	QueuePrt p;

	if (q->front == q->rear)//空佇列情況
		return;
	
	//非空佇列情況
	p = q->front->next;
	*e = p->data;
	q->front->next = p->next;

	if (q->rear == p)//佇列只一個元素,而元素被出隊了;在釋放該空間前
					 //將該尾指標移動回來,和頭指標指向同一節點頭結點
		q->rear = q->front;
	free(p);//釋放刪除節點所佔記憶體
}

//銷燬佇列
int DestoryQueue(LinkQueue* q) {
	if (1) {
		while (q->front) {
			q->rear = q->front->next;
			free(q->front);
			q->front = q->rear;
		}
		return 1;
	}
}

//建立一個連結串列並初始化,入隊操作,出隊操作並銷燬
int main() {
	LinkQueue s;

	initQueue(&s);

	char e,t;
	int i = 0;
	printf("請輸入字元並以#結尾:\n");
	scanf_s("%c", &e);

	while (e != '#') {
		InsertQueue(&s, e);
		i++;
		scanf_s("%c", &e);
	}

	while (i--)
	{
		DeleteQueue(&s, &t);
		printf("%c ", t);
	}

	int a=DestoryQueue(&s);
	if (a == 1) {
		printf("\n銷燬完畢\n");
	}

	system("pause");
	return 0;
}

#########################
不積矽步,無以至千里
好記性不如爛筆頭
截圖歸原作者所有,致謝。

相關文章