1. 程式人生 > 其它 >資料結構與演算法複習-第2-4章

資料結構與演算法複習-第2-4章

第二章 線性表

順序表

定義

T *aList;	//例項
int maxSize;	//最大長度
int curLen;	//當前長度
int position;	//當前處理位置

檢索

按位置:直接計算地址
按內容:遍歷。平均比較次數為(n+1)/2,平均時間開銷為O(n)

插入

  1. 檢查表是否溢位
  2. 檢查插入位置是否合法
  3. 從表尾開始將需要移動的元素向後移動一位
  4. 在指定位置插入新元素
    平均移動元素次數為n/2,時間開銷為O(n)

刪除

插入反向操作。

連結串列

定義

//結點
T data;
Link<T>* next;
//單鏈表
Link<T>* head, * tail;

檢索

無論按位置還是按內容都只能順序檢索。

插入/刪除

O(1)
雙鏈表和迴圈連結串列的討論類似。

第三章 棧與佇列

ADT

class Stack{
public:
	void clear();
	bool push(const T item);
	bool pop();
	bool top();
	bool isEmpty();
	bool isFull();
}

順序棧

需要事先知道或估算棧的大小。本質是簡化的順序表。
如果將第0個位置作為棧頂,則每次push和pop的時間代價都是O(n)
如果將最後一個位置作為棧頂,則每次push和pop的時間代價都是O(1)

  1. 儲存當前棧頂位置
  2. push或pop前檢查是否上溢或下溢

鏈式棧

本質是簡化的連結串列。棧頂元素應該設定為連結串列頭。
push和pop的代價都是O(1)

棧的應用:表示式求值

中綴表示式:

  1. 先執行括號內
  2. 無括號時,先乘除後加減
  3. 無優先運算子時,從左到右

字尾表示式(逆波蘭):

  1. 不含括號
    2 運算子放在參與運算的兩個語法成分後面
  2. 嚴格從左向右執行

中綴轉字尾:

  1. 運算元直接輸出到字尾表示式序列
  2. 開括號入棧
  3. 閉括號,清空棧至輸出序列
  4. 遇到運算子時,若(棧非空&&棧頂不是開括號&&棧頂運算子的優先順序不低於輸入的運算子的優先順序),迴圈彈出棧頂元素。結束上一過程後將輸入運算子入棧。
  5. 棧中剩餘元素彈出

理解方式:
比如說考慮3 op1 4 op2 5這個中綴表示式轉換成字尾表示式,前三步是固定的,即:輸出3、op1入棧、輸出4.
那麼處理op2的時候我們需要考慮要不要輸出op1,顯然如果輸出op1,字尾表示式就是3和4先算;不輸出就是4和5先算。所以如果op2的優先順序比op1更高,就不能先輸出op1。同理,假設這個中綴表示式延長,如果op3優先順序比op2高,那op2也不能輸出;平級的話按從左到右算,意味著平級的運算子是左邊的高於右邊(所以理解為什麼是不低於)。
書上的這個演算法描述挺繞的,實際上就是要維護這個運算子棧,保證:1.括號匹配,2.棧內運算子以開括號為界,優先順序嚴格遞增

棧與遞迴

遞迴由兩部分組成:遞迴基礎,即遞迴出口,也就是遞迴必須能夠結束;遞迴規則,也就是存在由高階到低階的計算規則。
考慮經典的揹包問題:揹包中可放入重量s,現有n件物品,重量分別為w0,...wn-1,求是否存在解使得揹包中重量之和剛好為s。
假設knap(n,s)存在解,考慮這個解和對n降階的解的關係,顯然把解中的物品拿掉一個就得到了一個n-1階的解,但我們無法確定被拿掉的是哪個物品,甚至根本不知道解裡有哪些物品,這種降階的計算規則是不可得的。
從另一個角度考慮降階:knap(n,s)的解中是否存在物品wn-1。如果否,則knap(n,s)等價於knap(n-1,s),相當於在沒有wn-1的集合求解同樣的問題;如果是,則knap(n,s)等價於knap(n-1,s-wn-1).
反過來想,這兩個子階問題中任意一個有解,則原問題也有解。所以遞推關係是knap(n,s) = knap(n-1,s)||knap(n-1,s-wn-1)
邊界條件: s=0, true; s!=0 && n=0, false

將遞迴用棧實現,本質就是把程式執行時棧轉換成程式中維護的棧結構。

//遞迴實現
bool knap(int n, int s){
	if(s==0) return true;
	if(n==0) return false;
	if(knap(n-1, s-w[n-1])){
		cout<<w[n-1];
		return true;
	}
	else return knap(n-1, s);
}

將遞迴轉化為棧最關鍵是要把棧增長的規則想清楚。設根結點(也就是主函式入口)的返回地址rd=0,knap(n-1,s-w[n-1])呼叫的返回地址rd=1,knap(n-1,s)呼叫的返回地址為2.
一開始會往棧裡一直壓1,直到s或n減到0。如果是s先減到0,說明找到可行解了,開始清棧,遇到rd=1的打印出w[knap.n-1]。如果n先減到0,說明棧頂的1無解,但是還可以壓入2,如果這個2有解,清棧列印;無解的話再壓下一級的2.

	stack<Node> mystack;
	mystack.push(Node(10,10,0));
	while(!mystack.empty()){
		Node top = mystack.top();
		if(top.s==0){
			while(!mystack.empty()){
				if(mystack.top().rd==1){
					cout<<w[mystack.top().n]<<" ";
				}
				mystack.pop();
			}
			break;
		}
		if((top.s<0 || top.n==0)&& top.rd==1){
			mystack.pop();
			Node tmp=mystack.top();
			mystack.push(Node(tmp.n-1,tmp.s,2));
		}
		else if((top.s<0 || top.n==0 )&& top.rd==2){
			mystack.pop();
			Node tmp=mystack.top();
			mystack.push(Node(tmp.n,tmp.s+w[tmp.n],2));
		}
		top = mystack.top();
		mystack.push(Node(top.n-1,top.s-w[top.n-1],1));
	}

只測了一個很簡單的demo,感覺思路上沒什麼問題。

佇列

只能在隊頭刪除,隊尾插入。

ADT

class Queue{
public:
	void clear();
	bool enQueue(const T item);
	bool deQueue(T& item);
	bool getFront(T& item);
	bool isEmpty();
	bool isFull();
}

順序佇列

可以利用%將佇列實現成一個環,這樣插入和刪除都可以在O(1)完成。不過還是要考慮溢位的問題。

鏈式佇列

和鏈式棧差不多。

第四章 字串

字串的儲存結構與實現

順序儲存

即char s[M]陣列,是定長的。
<string.h>提供了若干處理字串的常用函式。

//求s的長度,不計結束符
size_t strlen(char* s);
//將s2複製到s1,返回指標指向s1的開始
char* strcpy(char* s1, const char* s2);
//將s2拼接到s1尾部
char* strcat(char* s1, const char* s2);
//比較s1和s2
int strcmp(const char* s1, const char* s2);
//返回指向第一次出現在s中的c的指標
char* strchr(char* s, char c);
//逆向搜尋
char* strrchr(char* s, char c);

字串類class String

變長,動態管理長度。

字串的模式匹配

模式匹配問題,即對於字串T和模式P,在T中尋找P。假設T長度為n,P長度為m,通過對P預先處理的方式來降低複雜度。
也就是找P的字首P[0:i-1],找到其最長的字首和尾綴,使得P[0:k-1]=P[(i-k):(i-1)],這樣的P在P(i)和T失配時,我們可以直接P右移k位,保證不做多餘的比較,也不會漏掉匹配。
對於每一個i,將它的k值儲存在陣列next[i]中.
next陣列的求解可以遞推進行,假設next[i-1]=k,即P[0:k-1]=P[(i-k-1):(i-2)]是最長匹配對,現在我們要求解next[i],考慮P(k)和P(i-1)
(1)如果P(k)=P(i-1),則P[0:k]=P[(i-k-1):(i-1)],next[i]=k+1
(2)如果P(k)!=P(i-1),且k=0,則next[i]=0
(3)如果P(k)!=P(i-1),且k!=0,考察P(n[k])和P(i-1)的關係,迴圈(3)直至條件不滿足