1. 程式人生 > >資料結構學習之路——————串

資料結構學習之路——————串

1.串型別的定義

串(string)(或字串)是由零個或多個字元組成的有限序列,一般記為

                                        s='a1a2\cdot \cdot \cdot an'(n>=0)

 n稱為串的長度。零個字元的串稱為空串,長度為0。串中任意個連續字元組成的子序列稱為該串的子串。包含子串的串相應地稱為主串。通常稱字元在序列中的序號為該字元在串中的位置。稱兩個串相等時,長度相同,對應位置字元相同。串值需由單引號括起來,但單引號不屬於串。

串的邏輯結構和線性表極為相似,區別在於串的資料物件約束為字符集。然而基本操作有很大區別。線上性表的基本操作中,大多以“單個元素”作為操作物件;而串中,通常以“串的整體”為操作物件。

2.串的表示和實現

2.1 定長順序儲存表示

類似線性表的順序儲存結構,用一組連續的儲存單元儲存串值得字串序列。按照預定義的大小,每個定義的串將被分配一個固定長度的儲存區。

//------------串的定長順序儲存表示-------------
#define MAXSTRLEN 255         //使用者可在255以內定義最大串長
typedef unsigned char SString[MAXSTRLEN + 1];//0號單元存放串的長度  

串的實際長度可在預定義長度的範圍內隨意,超過預定義長度的串值則被捨去,超過預定義長度的串值則被捨去,稱之為“截斷”。對串長有兩種表示方法:一是如上述定義描述那樣,以下標為0的陣列分量存放串的實際長度;二是在串值後面加一個不記入串長的結束標記字元,如“\0”作為結束標記字元。

2.2 堆分配儲存表示

這種儲存方式的特點是,仍以一組地址連續的儲存單元存放串值字元序列,但他們的空間是在程式執行過程中動態分配而得的。在C語言中,存在一個稱之為“堆”的自由儲存區,並由C語言的malloc()和free()來管理(C++中新增new)。

//--------------串的堆分配儲存表示------------------
typedef struct {
	char *ch;   //若為非空串,則按串長分配儲存區,否則ch為NULL
	int length;
}HString;
#include<string>
#include<iostream>
#include <cstdlib>
#include <cstdio>
#include<memory>
//------------串的定長順序儲存表示-------------
#define MAXSTRLEN 255         //使用者可在255以內定義最大串長
typedef unsigned char SString[MAXSTRLEN + 1];//0號單元存放串的長度  
//--------------串的堆分配儲存表示------------------
typedef struct {
	char *ch;   //若為非空串,則按串長分配儲存區,否則ch為NULL
	int length;
}HString;

//--------------------------基本操作的演算法描述--------
bool StrAssign(HString &T,char *chars){
	int n;
	//生成一個其值等於串常量chars的串T
	if (T.ch)free(T.ch);//釋放T原有空間
	for (int i = 0, char *c = chars; *c; c++, i++) { n = i; }//求chars的長度i
	if (!n) { T.ch = NULL; T.length = 0; }
	else {
		if (T.ch = (char*)malloc(n * sizeof(char)))
			exit(OVERFLOW);
		for (int i = 0; i < n; i++) {
			T.ch[i] = chars[i];
		}
		T.length = n;
	}
	return 1;
}

int StrLength(HString S)
{
	return S.length;
}

bool ClearString(HString &S) {
	//將S清為空串
	if (S.ch) { free(S.ch); S.ch = NULL; }
	return 1;
}

bool Concat(HString &T, HString S1, HString S2) {
	//用T返回由S1和S2聯接而成新串
	if (T.ch)free(T.ch);
	if (!(T.ch = (char*)malloc((S1.length + S2.length) * sizeof(char))))
		exit(OVERFLOW);
	for (int i = 0; i < S1.length; i++) {
		T.ch[i] = S1.ch[i];
		T.length = S1.length + S2.length;
		for (int i = S1.length,int j=0; i < T.length,j<S2.length; j++,i++) {
			T.ch[i] = S2.ch[j];
		}
	}
	return 1;
}

bool Subtring(HString &Sub, HString S, int pos, int len) {
	//用sub返回串S的第pos個字元起長度為len的子串
    //其中1<=pos<=StrLength(S)且0<=len<=StrLength(S)-pos+1
	if (pos<1 || pos>S.length || len<0 || len>S.length - pos + 1)
		return 0;
	if (Sub.ch)free(Sub.ch);
	if (!len) {
		Sub.ch = NULL; Sub.length = 0;
	}
	else {
		Sub.ch = (char*)malloc(len * sizeof(char));
		for (int i = 0, int j = pos - 1; i < len, j < pos+len - 1; i++, j++)
		{
			Sub.ch[i] = S.ch[j];
			Sub.length = len;
		}
	}
	return 1;
}

2.3 串的塊鏈顯示

和線性表的鏈式儲存結構相類似,結構中的每個資料元素是一個字元,則用連結串列儲存的結點可放一個或多個字元。當結點大小大於1,由於串長不一定是結點的整數倍,則連結串列中的最後一個結點不一定被串值佔滿,此時通常不上“#”或其他非串值字元。

為了便於進行串的操作,當以連結串列儲存串值時,除頭指標外還可附設一個尾指標指示連結串列中的最後一個結點,並給出當前串的長度。稱如此定義的串儲存結構為塊鏈結構。

#define CHUNKSIZE 80   //可由使用者定義的塊的大小
typedef struct Chunk {
	char ch[CHUNKSIZE];
	struct Chunk *next;
	}Chunk;
typedef struct {
	Chunk *head, *tail; //串的頭指標和尾指標
	int curlen; //串的當前長度
}LString;

一般情況下,對串進行操作時,只需從頭向尾順序掃描即可,則對串值不必建立雙向連結串列。設尾指標是為了便於進行聯結操作,但應注意聯結時需處理第一個串尾的無效字元。

儲存密度=串值所佔的儲存位/實際分配的儲存位

2.4 串的模式匹配演算法

子串的定位操作通常被稱為串的模式匹配(其中T稱為模式串)。

int Index(SString &S, SString T, int pos) {
	//返回子串T在主串S中第pos個字元之後的位置。
	int i = pos; int j = 1;
	if (i <= S[0] && j <= T[0]) {
		if (S[i] == T[j]) { i++; j++; }
		else {
			i = i - j + 2; j = 1;
		}//如不等指標後退重新開始匹配,是退到下一個字元上
	}
	if (j > T[0]) return i - T[0]; //i為匹配的子串在主串的最後位置,減去T長度即為位置
	else return 0;
}

模式匹配的一種演算法:如上述程式碼所示。其最壞複雜度為O(n*m),n,m分別為主串和模式串的長度。

模式匹配的一種改進演算法:KMP演算法

此演算法可以在O(n+m)的時間數量級上完成串的模式匹配操作。其改進在於:每當一趟匹配過程中出現的字元比較不等時,不需回溯i指標,而是利用所得的“部分匹配”的結果將模式串儘可能遠地向右滑動一段距離後,再進行比較。

改進演算法解決下述問題:當匹配過程中產生失配,模式串“向右滑動”的距離有多遠,即當主串中第i個字元與模式中第j個字元失配時,主串中第i個字元(i指標不回溯)應與模式中的哪個字元比較。

現在討論一般情況:假設此時與模式中第K個字元繼續比較,則前k-1個字元要相匹配,即滿足下列關係式

'{p_{1}}{p_{2}}\cdot \cdot \cdot {p_{k-1}}'='{s_{i-k+1}}{s_{i-k+2}}\cdot \cdot \cdot {s_{i-1}}'

而已得到的匹配結果是:'{p_{j-k+1}}{p_{j-k+2}}\cdot \cdot \cdot {p_{j-1}}'='{s_{i-k+1}}{s_{i-k+2}}\cdot \cdot \cdot {s_{i-1}}'(?先比完整段模式串?)

由上式推得:'{p_{1}}{p_{2}}\cdot \cdot \cdot {p_{k-1}}'='{p_{j-k+1}}{p_{j-k+2}}\cdot \cdot \cdot {p_{j-1}}'

反之,若模式串存在滿足上式的兩個子串,則當匹配過程中,主串中的第i個字元與模式串中的第j個字元比較不等時,僅需將模式向右滑行到模式中第k個字元和主串中第i個字元對齊,此時,模式中頭k-1個字元的子串必定與主串中第i個字元之前長度為k-1的子串相等,由此僅需從模式第k個字元與主串第i個字元比較起繼續進行。(翻譯一下,在失配字元前即模式串中包含兩端相同的子串且與主串匹配,下次移動時就可直接把前一段串移動到後面相等的串的位置上)。

  若令next[j]=k,則next[j]=k表明當模式中第j個字元與主串“失配”時,在模式中需要重新和主串中該字元進行比較的字元的位置。

             0      當j=1 

next[j]=MAX{k|1<k<j且'{p_{1}}{p_{2}}\cdot \cdot \cdot {p_{k-1}}'='{p_{j-k+1}}{p_{j-k+2}}\cdot \cdot \cdot {p_{j-1}}'}

            1     其他情況

上式可求出next[j] 陣列的值。求得該函式之後,匹配可如下進行:假設以指標 i 和 j 分別指示主串和模式中正待比較的字元,令i的初值為pos,j 的初值為1。若在匹配過程s_{_{i}}=p_{_{j}},則 i 和 j 分別增1,否則 i 不變,j 再退到下一個next值的位置,以此類推,直至下列兩種可能:一種是 j 退到某個next值時字元比較相等,則指標各自增1,繼續進行匹配;另一種是 j 退到0,則此時需將模式繼續向右滑動一個位置。

int Index_KMP(SString S, SString T, int pos,int next[]) {
	int i = pos;int j = 1;
	while (i <= S[0] && j <= T[0]) {
		if (j == 0 || S[i] == T[i]) { i++; j++; }
		else j = next[j];
	}
	if (j > T[0])return i - T[0];
	else return 0;
}
int get_next(SString T, int next[]) {
	//求模式串T的next函式值並存入
	int i = 1; next[1] = 0;
	int j = 0;
	while (i < T[0]) {
		if (j == 0 || T[i] == T[j]) { ++i; ++j; next[i] = j; }
		else j = next[j];
	}
}
void get_nextval(SString T, int nextval[]) {
	//求模式串T的next函式修正值並存入nextval中
	int i = 1; nextval[1] = 0;; int j = 0;
	while (i < T[0]) {
		if (j == 0 || T[i] == T[j]) {
			++j; ++i;
			if (T[i] != T[j])nextval[i] = j;//增加了一個判別條件,相等時nextval為前值
			else nextval[i] = nextval[j];
		}
		else j = nextval[j];
	}
}