數據結構(12.1)串結構的定義

前言

串是一種比較特殊的數據結構,它是計算機上非數值處理的主要對象。

事實上在C語言中,是沒有“字符串”這個變量類型的,雖然可以通過字符指針 char *p 這樣來記錄一個字符串,p本質上仍是一個字符指針,保存的是一個字符的地址。因此,C語言中的保存一個字符串,只是保存它首個字符的地址。但是到了諸如Java這樣的語言裏,提供了一種String類,可以直接用String s 來記錄字符串,String和int、char一樣是一種數據類型(只不過不是基本的數據類型,而是屬於引用類型)。並且Java提供了很多操作函數,可以直接實現對字符串的相關操作。當然,C語言中雖然沒有明確的字符串類型,但是同樣存在字符串操作函數。

串的定義

  • 定義:串(或字符串)是由零個或多個字符組成的有限序列,一般記爲:s = ‘a1a2…an’(n>=0)
  • 串的名稱:s
  • 串的值:用單引號(有些書中也用雙引號)括起來的字符序列,可以是數字、字母或者其他字符。但是單引號本身不屬於串,它的作用是避免與變量名或數的常量混淆。
  • 串的長度:n

有關概念

  • 空串:零個字符的串稱爲空串,其長度爲零。例如 s = ‘’。

  • 子串:串中任意個連續的字符組成的子序列稱爲該串的子串,例如 s = ‘abcde’,t = ‘abc’,則 t 是 s 的子串

  • 主串:包含子串的串相應地稱爲主串,例如 s = ‘abcde’,t = ‘abc’,那麼 s 就是 t 的主串

  • 串的位置:字符在序列中的序號稱爲該字符在串中的位置,例如s = ‘abcde’,字符’ b '在 s 中的位置就是2(在實際應用中爲了方便數組操作,也可以從零開始數,則此處就爲1)。

    而子串在主串中的位置,則用子串的第一個字符在主串中的位置來表示。例如 s = ‘abcde’,t = ‘cd’,子串 t 在 s 中的位置爲3。

  • 串相等:只有兩個串的長度相等,並且各個對應位置的字符都相等時才稱爲串相等。例如 s = ‘abc’,t = ‘ab c’,此時 s 和 t 並不相等(空格也屬於字符集合中的一個元素)。

  • 空格串:由一個或者多個空格組成的串,稱爲空格串。例如 s = ’ ',此時 s 串是空格串,但不是空串,其長度爲空格的數量。

串的存儲結構

回顧線性表的概念:由n(n>=0)個數據特性相同的元素構成的有限序列。

可以發現,同樣是有限序列,串的邏輯結構和線性表是很相似的,區別僅在於串的約束對象是字符集。也就是說,假如字符串中存儲的是’123456’這麼一串,實際上它保存的類型是char,而不是int。

那麼,把線性表的存儲類型改爲char不也能表示串嗎?按理來說是這樣,但是串與線性表的主要區別是在操作上。在線性表的基本操作中,多以”單個元素“作爲操作的對象,例如插入、刪除等,多是對某個元素進行操作;而在線性表中,多以“串的整體”作爲操作的對象,比如插入,多是插入另一個串,而不是單個字符。因此,串的操作和線性表有很大不同,需要把串當成與線性表不同的邏輯結構來看待。

從存儲結構上來說,串也有兩種基本的存儲結構:順序存儲和鏈式存儲。

順序存儲

有些資料中說,串有三種機內表示方法,分別是定長順序表示、堆分配存儲表示和塊鏈存儲表示。實際上,定長順序表示和堆分配表示都屬於順序存儲。

順序存儲的特點是,使用一組地址連續的存儲單元來進行存儲。我們使用順序存儲結構時,往往需要在一開始去申請一整塊的內存空間,然後在我們所申請好的這塊內存空間內來插入、刪除元素;這樣就會出現一個問題:如果這塊空間滿了,又有新元素需要存入,應該怎麼辦呢?

在我們以往的實現中,有兩種做法:第一種是邏輯上我的這個存儲空間已經滿了,就不允許再存入了,它的內存是靜態的,能存多少元素已經在一開始確定了。而第二種則會在空間滿的時候去重新申請一塊更大的空間,只有當物理內存真正滿了,空間申請失敗的時候,纔不允許再存入,它的內存是動態分配的。

我們把採用第一種處理方法稱爲定長順序表示,定長即串空間的大小在編譯的時候就確定了,不允許再更改,順序則意味着它的地址空間是連續的。而第二種處理方法稱爲堆分配表示,因爲C語言的內存分爲五大分區,其中一個區叫做堆(heap)區,這個區的空間可以讓程序員來進行分配釋放,我們調用malloc和free函數,就是在對堆區的內存進行操作。在堆分配表示中,串的空間可以動態分配,意味着串長可以任意(只要不超過實際的內存),但是它的地址空間仍然是連續的。

定長順序表示

在定長順序表示裏,需要規定一個最大串長,在串創建時就按照預定義的大小爲每個串分配固定的存儲區,串的實際長度可以在預定義的範圍之內,超過預定義長度的部分將會被捨去,稱爲”截斷“。

堆分配表示

在堆分配表示中,存儲空間是在程序執行過程中動態分配而來的,所佔的空間由串的實際長度決定。需要注意的是,即使是動態分配,它的空間也是連續的一整塊,可以去查一下malloc、relloc和calloc函數的細節。

順序存儲中串結構的設計

無論是順序存儲還是鏈式存儲,都首先需要考慮一個問題:怎樣確定串已經結束了?實際上它也可以轉換成另一個問題:怎樣確定串的實際長度?針對這兩個問題的不同回答,決定了對串結構的不同設計。在這裏先說順序存儲中的情況。

假設我們從串長的角度來考慮,只需要額外使用一個變量來保存串的長度,並且實時更新就可以了。這樣,我們可以設計出來一個結構體,它包括一個存儲串的空間,和一個保存串當前長度的整型變量

定長順序表示:

#define MAXSTRLEN 255 //最大串長
typedef struct SString{
  char ch[MAXSTRLEN]; //存儲串的一維數組
  int length;      //串的當前長度
}SString;

堆分配表示:

typedef struct HString{
    char *ch;  //串的存儲空間
    int length;  //串的當前長度
}HString;

在定長順序串中,還可以直接把當前長度存到數組中的0號單元裏,讓串從1下標開始,這樣一開始要開闢的就是最大串長+1的空間,並且也不需要結構體了。

#define MAXSTRLEN 255 //最大串長
typedef unsigned char SString[MAXSTRLEN+1]; //0號單元存放串的當前長度

img_1

需要注意,這個時候使用 unsigned char 型來保存數值,最大串長只能到255。

假如從串結束的問題來考慮,可以和C語言的做法一樣,在串的最後加上一個不計入串長的字符來標記,比如’\0’,這樣要開闢的也是最大串長+1的空間。由於這個時候的串長是無法直接得到的,在有些操作上可能不太方便。

定長順序表示:

#define MAXSTRLEN 255 //最大串長
typedef unsigned char SString[MAXSTRLEN+1]; //多出來一位用於在串滿時在末尾加上'\0'

img_2

堆分配表示:
(初始化時需要記得多申請一個位置用於存放’\0’,並且在每次串變動後都需要注意’\0’的設置。)

typedef unsigned char* HString;

鏈式存儲

一般而言,鏈式存儲的設計包括兩個部分:第一是結點的設計,包括結點要保存的內容和後續結點的指針。第二則是針對整個鏈式結構的設計,比如設置頭指針尾指針、結點數量等,用於對整個結構進行管理,並且方便一些操作。

額外提一下,這個“結點要保存的內容”可以很簡單,我們平時學習的時候爲了簡便常常只是設置一個int型,得到的只是一些數值,感覺似乎沒什麼用;但是它也可以比較複雜,例如一個學生的信息,那麼就需要char來記錄名字,int來記錄年齡、編號等等等等,需要根據實際情況來設置這個結構體,它的應用其實是很廣泛的。

言歸正傳,對結點的設計很重要。按照往常的思路,我們一個結點用於保存一個字符,那麼就是這樣的:

img_3

//結點
typedef struct Node{
  char c; //保存字符
  struct Node * next; //下一個結點
}Node;

這雖然可行,但是我們一個結點只保存一個字符的話,每保存一個字符,就要花一個額外的空間來保存下一個結點的指針,造成比較大的浪費,由於串的特性,我們也可以考慮一個結點保存多個字符。

img_4

一個結點保存多個字符,首先需要設置結點可保存的最大字符量,假如我們用數組來存儲,這實際上就是定長順序串和鏈式存儲的結合。

#define CHUNCKSIZE 3 //一個結點保存三個字符
//結點
typedef struct Chunk{
  char ch[CHUNKSIZE]; //存儲串的一維數組
  struct Chunk *next; //下一個結點
}Chunk;

這裏可以通過判斷next結點是否爲空來確定串是否結束,所以不像順序存儲中那麼麻煩。

img_5
如圖所示,假如我們使用數組來存儲的話,最後一個結點它不一定會被佔滿,此時常補上“#”或者其他的非串值字符(通常不把“#”包括在串的字符集中,而是當做一個特殊符號。)

如果不想造成這樣的浪費的話,也可以考慮不用數組來存儲,而是動態開闢空間,假如要存儲的字符串長度大於等於設定的結點長度,就按照設定長度來開闢;假如小於,則按照字符串實際的長度來開闢(怎樣確定要存儲的字符串長度,又是一個問題)。

#define CHUNCKSIZE 3 //一個結點保存三個字符
//結點
typedef struct Chunk{
  char *ch; //實際空間->按照情況來分配
  struct Chunk *next; //下一個結點
}Chunk;

img_6

爲了方便串的操作,除了頭指針外還可以附設一個尾指針,並且給出當前串的長度,稱這樣定義的串存儲結構爲塊鏈結構,因爲單個結點它不是隻保存一個數據,而是一“塊”數據。

//整個串
typedef struct LString{
  Chunk *first; //頭指針
  Chunk *last; //尾指針
  int length;  //串長
}LString;

串的鏈式存儲結構在除了某些操作,比如連接兩個串時,有一定的方便之處,但是總體而言沒有順序存儲結構靈活,性能也不高,因此比較少用。

發佈了36 篇原創文章 · 獲贊 18 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章