数据结构杂谈(二)
本文的所有代碼均由C++編寫
如果你已經看完這篇雜談,可以前往下一篇→數據結構雜談(三)_塵魚好美的小屋-CSDN博客
文章目錄
- 2 順序表
- 2.1 線性表的類型定義
- 2.2 類C語言有關操作補充
- 2.2.1 ElemType的解釋
- 2.2.2 數組定義
- 2.2.3 建立鏈表可能會用到的函數
- 2.2.4 參數傳遞問題
- 2.2.4.1 地址傳遞
- 2.2.4.2 值傳遞
- 2.2.4.3 數組名為參
- 2.3 順序表
- 2.3.1 順序表的定義
- 2.3.1.1 順序存儲方式
- 2.3.1.2 靜態分配方法
- 2.3.1.3 數據長度和線性表長度區別
- 2.3.1.4 動態分配方法
- 2.3.1.5 順序表的特點
- 2.3.2 順序表的基本操作
- 2.3.2.1 初始化
- 2.3.2.2 順序表的插入
- 2.3.2.3 順序表的刪除
- 2.3.2.4 順序表的按位查找
- 2.3.2.5 順序表的按值查找
2 順序表
下面開始我們要學習一種最常用也是最簡單也是最不簡單的邏輯結構,也就是線性表。
為了更直觀地體現線性表,我們舉幾個生活中的例子。
第一個例子是食堂排隊打飯,這種排隊通常都是一個接著一個,第一個同學打完就輪到下一個同學打飯,如果隊伍中間一個人有事走了,那么其他人都會前進一步,而如果前面有人仗著有朋友而插隊,那么所有人都得后退一步,這種情況在線性表中的體現即為順序表。
第二個例子是醫院掛號,這種掛號一般是按順序掛號,掛完號的人就去等待區候著而不用在那里排隊,到了你的號數就去看病,與前一種方式不同,它不會因為你站的位置來決定你的看病的先后順序,而是看你手中的掛號牌,這種情況在線性表中的體現即為單鏈表。
2.1 線性表的類型定義
接下來我們要講的東西是針對線性表,即單鏈表和順序表都屬于此范疇。
線性表是具有相同數據類型的n個數據元素的有限序列。其中n為表長,當n=0的時候線性表是一個空表。若用L命名線性表,即:L=(a1,a2,a3….,an)L = (a_1,a_2,a_3….,a_n)L=(a1?,a2?,a3?….,an?)。
關于上面的L表示法,其中有幾個術語是需要我們知道的。
a2相對于a1來說排在后面,所以我們叫做直接后繼。a1相對于a2來說排在前面,所以我們叫他直接前驅。很顯然,倒數第二個元素只有一個直接后繼,第二個元素只有一個直接前驅。在非空表里每個數據元素都有自己對應的位置,我們叫做位序。比如在線性表中,a1排在第一位,我們說a1排在線性表中的第一位序。
位序和索引要明確,線性表中第一個元素即為第一位序,且為第0個元素。
2.2 類C語言有關操作補充
在考研必備教材《數據結構C語言版》嚴蔚敏版中,為了方便同學的學習,采用了類C語言,即C語言C++混用,不在意語法;意在讓同學理解內在邏輯而不過分追究編譯語法。
2.2.1 ElemType的解釋
我們給出一個線性表中順序表的定義,這里我們只是簡單了解下,看不懂沒關系。
typedf struct{ElemType data[];int length; }SqList;//順序表類型這時候我們會覺得很奇怪,從來沒有看過ElemType這種類型。實際上我們從英文上可以看出,這里的意思是元素類型,即數據元素類型,比如說你要用什么數組,如果你的數組打算放int,那么你可以把ElemType的位置換成int。
2.2.2 數組定義
typedf struct{ElemType data[MaxSize];int length; }SqList;//順序表類型從上面的代碼來看,很明顯使用數組來表示順序表,用長度來記錄表長。但是它確有另外一種表示方法。
typedf struct{ElemType *data;int length; }SqList;//順序表類型實際上,如果是data[MaxSize],那么我們會發現一旦MaxSize確定下來了,那么我們的數組長度也就確定下來了。
但是如果我們用的是* data,那么data是一個指針變量,我們可以通過new函數來申請一片內存的地址,然后把地址賦給data數組,這樣數組的長度由數組元素的字節長度和數組的最大容納量來確定了。這實際上就是靜態數組分配和動態數組分配的區別,后面會講到。
2.2.3 建立鏈表可能會用到的函數
在C語言中我們常用的是
-
malloc函數,用于開辟m字節長度的地址空間,并且返回這段空間的首地址。
-
sizeof函數,計算變量x的長度
-
free§函數,釋放指針p所指變量的存儲空間,即徹底刪除一個變量。
需要注意的是,如果要用到以上的函數,需要加載頭文件:<stdlib.h>
而在C++中,我們用的是
- new函數
- delete函數
- sizeof函數
2.2.4 參數傳遞問題
2.2.4.1 地址傳遞
地址傳遞實際上就是傳地址給函數,函數通過指針的解引用互換對應地址中的值;但是這里有個問題,地址不能互換,只有地址上的值能換,這是需要注意的一點。
對于C語言來說,通常喜歡用指針來修改傳入的參數并返回對應的結果,而對于C++來說,用引用&會更方便一些。
在C++中,引用相當于一個別名,比如說int a = 1,此時你用引用可以給1再起一個別名,比如起個別名叫b,那么對b中的1修改為2后,a的1也會編程2,這樣相當于是用兩個名字去操縱同一個數據。
引用類型作形參,在內存中并沒有產生實參的副本,他直接對實參操作;而一般變量作參數,形參與實參就占用不同的存儲單元,所以形參變量的值是實參變量的副本。因此,當參數傳遞的數據量較大時,用引用比用一般變量傳遞參數的時間和空間效率都好。
指針參數雖然能達到和使用引用類型的效果,但在被調函數中需要重復使用“指針變量名”的形式進行計算,這很任意產生錯誤且程序的閱讀性較差;另一方面,在主調函數的調用點處,必須用變量的地址作為實參。
2.2.4.2 值傳遞
值傳遞是把數值傳進函數,這樣的做法在函數里面的確能夠實現應有的功能,但是當函數運行結束時,變量不會做任何改變,因為變量傳給函數的是數值,變量所在的地址的數值仍未發生變化。
2.2.4.3 數組名為參
我們都知道,數組的名字實際上代表著數組中首元素的地址,所以對形參數組所做的任何改變都會反映到實參數組中。
我們都知道,數組的名字實際上代表著數組中首元素的地址,所以對形參數組所做的任何改變都會反映到實參數組中。
#include <iostream> using namespace std; void change(char a[]) {a[0] = 'a'; }int main() {char arr[] = { '1','2','3','4' };cout << "改變前的arr[0]:" << arr[0]<<endl;change(arr);cout << "改變后的arr[0]:" << arr[0] << endl;system("pause");return 0;} 結果: 改變前的arr[0]:1 改變后的arr[0]:a2.3 順序表
說那么多線性表,我們接下來來看看線性表的兩種物理結構其中之一:順序存儲結構。
2.3.1 順序表的定義
線性表的順序表示指的是用一組地址連續的存儲單元依次存儲線性表的數據元素。
其示意圖如下:
這種表示一般被我們叫做順序存儲結構或者叫做順序映像。擁有這種結構的線性表我們叫順序表。
2.3.1.1 順序存儲方式
線性表的順序存儲結構,說白了就是在內存中隨便找塊地,通過占位的形式,把一定內存空間給占了,然后把相同數據類型的數據元素依次存放到這塊空地中。這里我們想到,這有點類似于一維數組吧?一維數組也是這個定義。
這里實際上隱含著另外一個意思,既然線性表的數據元素都是相同數據類型,那么我們知道一個數據元素占幾個字節(數據元素的大小),那么線性表連續,就必定有LOC(ai+1)=LOC(ai)+lLOC(a_ {i+1}) = LOC(a_i)+lLOC(ai+1?)=LOC(ai?)+l(這里的loc指的是內存地址,即location)這種情況,其中lll代表他們一個數據元素的大小。比如一維數組a[1],a[2]。如果是整型數組,那么就有如下結果:
至于lll所代表的數據元素的大小我們從何而知呢?就是利用我們在2.2.3講到的sizeof函數。
2.3.1.2 靜態分配方法
在2.2.2時我們曾經提到兩種順序表的實現方法,即動態分配和靜態分配,在下面我們會依次講解這兩種方法。
我們前面說過可以用一維數組來表示線性表,但是這里要注意一點,線性表長可變,而數組長度是不可動態定義。這時候我們用一個變量(length)來表示順序表的長度屬性。所以單純用一個數組還不行,還要加一個長度屬性。
那如何用C語言去定義順序表呢?線性表每個節點中都有數據,可是沒有說是什么數據類型,可以是基本數據類型也可以是復合數據類型;可以是結構化數據類型也可以是半結構化數據類型,所以,我們通常利用結構體來創建一個線性表,其中線性表包含數據元素和長度。所以如果寫成代碼如下:
#define MAXSIZE 100 //定義數組的最大長度 typedef struct{ElemType data [MAXSIZE]; //用靜態的數組存放數據元素int length; //順序表當前長度 }SeqList;在以上的定義中,我們可以發現一件事。如果我們過早的指定一維數組中的MAXSIZE,那么我們很難擔保后面能夠提供足夠多的數據元素。
就拿占位的例子來說,一個人占了九個位置給舍友,這只是一個估計,是死的、理想狀態的;而實際上,九個人并不是那么好學,里面有一些人沒來放鴿子;還有一種情況就是,九個人有幾個還帶了女朋友(單身狗留下了淚水),那占的位置不夠坐的情況也有可能發生。
在上一段話中提到線性表數據元素不足和數據元素存滿的情況,數據元素不足只是浪費了內存,如果是存滿這時候就可以放棄治療了。
2.3.1.3 數據長度和線性表長度區別
對于上面的講解可能還有些人會有疑惑,length和MAXSIZE的區別不是很清楚,在這一小節我們詳細闡述一下。
靜態分配如下所示:
#define MAXSIZE 100 typedef struct{ElemType data [MAXSIZE];int length; }SeqList;我們在這里發現定義順序表需要三個屬性:
- 存儲空間的起始位置:數組data
- 線性表的最大存儲容量:數組長度MaxSize
- 線性表的當前長度:length
也就是說,你確定了數組長度是吧,數組開辟空間了是吧,最后數組上面選一段作為線性表,有點套娃,如圖所示:
也就是說,如果你數組開辟的空間不夠多,就會導致順序表用的空間不夠多,也就會導致順序表的數據元素填不進去。
所以綜上所述,我們得出如下結論:
- 數組長度是指存放線性表的存儲空間的長度,存儲分配后這個值是不變的。
- 線性表的長度是線性表中數據元素的個數,隨著線性表的插入與刪除,這個值是在變換的。
2.3.1.4 動態分配方法
講完了靜態分配方法,我們來說說動態分配方法。
由于線性表強調元素在邏輯上緊密相鄰,所以我們最開始想到用數組存儲。但是普通數組有著無法克服的容量限制,在不知道輸入有多少的情況下,很難確定出一個合適的容量。對此,一個較好的解決方案就是使用動態分配,即動態數組。
使用動態數組的方法是用new申請一塊擁有指定初始容量的內存,這塊內存用作存儲線性表元素,當錄入的內容不斷增加,以至于超出了初始容量時,就用new擴展內存容量,這樣就做到了既無浪費內存,也可以讓線性表容量隨輸入的增加而自適應大小。
在這里我們先給出動態分配的定義,至于怎么擴充,我們后面會講。動態分配方法如下:
#include <iostream> using namespace std; #define InitSize 10 //默認的最大長度//順序表結構體定義 typedef struct {//指示動態分配數組的指針int* data;//順序表的最大容量int Maxsize;//順序表的當前長度int length; }SeqList;//初始化順序表 void InitList(SeqList& L) {L.data = new int[InitSize * sizeof(int)];L.length = 0;L.Maxsize = InitSize; }如果想要增加動態數組的長度,可以編寫如下函數:
void IncreaseSize(SeqList& L, int len) {int* p = L.data;L.data = new int[(L.Maxsize + len) * sizeof(int)];for (int i = 0; i < L.length; i++){L.data[i] = p[i]; //將數據復制到新區域}L.Maxsize = L.Maxsize + len; //順序表最大長度增加lendelete(p); //釋放老數組的內存空間 }原來老數組是在內存中開辟了一塊內存空間,而我們在增加動態數組的長度時,實際上是在內存的其他地方,開了一塊更大的空間,然后把老數組上面的數組復制過去新數組。
既然如此,我們就應該把定義一個新指針p,把老數組的指針移交給p后,把data指針指向新數組。此時p指老,data指新,通過循環把p中的每一個元素移交給data即可,移交完成后,要記得把順序表的最大容量也修改一下,即老順序表的最大容量加上擴充容量。
經過上面的代碼講解,我們可以知道一件事就是,由于要把數據從老數組復制到新數組,實際上時間開銷是非常大的,這也對應了我們2.3.1.3講解的,一般來說在一些書上是不會講這個動態分配的事的,只有在考研中才會涉及到這個知識點。
2.3.1.5 順序表的特點
結果上面的講解,我們可以總結順序表具有如下特點:
- 隨機訪問,即可以在O(1)時間內找到第i個元素
- 存儲密度高,每個節點只存儲數據元素
- 拓展容量不方便(即便采用動態分配的方式實現,拓展長度的時間復雜度也比較高)
- 插入、刪除操作不方便,需要移動大量元素
2.3.2 順序表的基本操作
實際上,順序表和單鏈表的操作基本都是這幾個原理。在這里,我們先講順序表的基本操作。
在這之前我們需要介紹一下操作算法中用到的預定義常量和類型,在后面的代碼中,你經常會看見這些字眼。
//函數結果狀態代碼 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 //Status 是函數的類型,其值是函數結果狀態代碼 typedef int Status; typedef char ElemType;2.3.2.1 初始化
這里的初始化其實包含了兩個動作:對順序表的初始化和對數組的初始化。在前面我們知道順序表是用數組表示的,這就意味著數組確定了空間大小后,如果順序表沒用滿數組中的內存,就勢必有一些內存有臟數據。所有在使用順序表之前,你需要先把數組內所有元素設為0,并且把順序表長度設為0。
#include <iostream> using namespace std; #define MaxSize 10//定義數組最大長度typedef struct {int data[MaxSize];int lenght; }SqList;//初始化 void InitList(SqList& L) {for (int i = 0; i < MaxSize; i++){L.data[i] = 0; //將所有數據元素設置為默認初始值0}L.lenght = 0; //順序表初始長度為0 }int main() {SqList L;InitList(L);for (int i = 0; i < MaxSize; i++)cout << L.data[i] << endl;return 0; }實際上,上面的操作具有一定的違規,因為我們是在用順序表,而不是在用數組,所以上面打印的條件應該是i<L.length。所以,這里我們又可以發現,我們只是用順序表而不用數組的話,實際上是不會訪問到臟數據的,所以初始化一般都是初始化順序表的長度,而不用去初始化數組的值。
然而,拋開上面不談,實際上我們訪問數據的方式也不夠好,在測試階段我們的確可以用這種方式,但是實際做題或者其他應用中,我們還是得用基本操作去訪問數據元素。在下一小節,我們就會講到這個問題。
2.3.2.2 順序表的插入
插入刪除在順序表中其實很簡單,你可以想象這么一個場景:你們在買火車票,有個人想插隊到你前面,一旦你同意,你后面排隊的人都得退一步;而如果你們在排隊,有個人有事突然走了,那么所有排隊的人都可以前進一步。這就是順序表的插入刪除。其中插入的操作有些人俗稱加塞,示意圖如下:
ListInsert(&L,I,e):插入操作。在表L中的第i個位置上插入指定元素e。
bool ListInsert(SeqList& L, int i, int e) {if (i<1 || i>L.length + 1)//判斷i的范圍是否有效return false;if (L.length >= MAXSIZE)//當前存儲空間已滿,不能插入return false;for (int j = L.length; j >= i; j--)L.data[j] = L.data[j - 1];L.data[i - 1] = e;L.length++;return true; }注意:在增加元素的時候一定要檢查插入之前是否存滿了,為此,我們在上面的代碼合理性判斷。
說明:檢查i的合法性判斷。好的算法,應該具有“健壯性”。能處理異常情況,并且給使用者反饋。
關于插入操作的時間復雜度,通常都是直接看最內層循環。
-
如果考慮最好情況:新元素插入到表尾,不需要移動元素,i = n+1,循環0次;最好時間復雜度為O(1)
-
最壞情況:新元素插入到表頭,需要將原有的n個元素全都向后移動,i = 1,循環n次,最壞時間復雜度 為O(n)
-
平均情況:假設新元素插入每個位置的概率都相等,即p = 1/(n+1),i = 1,循環n次,i = 2,循環n-1次,也就是1+2+3+…+n,根據等差數列求和公式,也就是n(n+1)/2
平均循環概率 = 平均復雜度 = np = n/2 = O(n)
2.3.2.3 順序表的刪除
ListDelete(&L,I,e):刪除操作。刪除表中第i個位置的元素,并用e返回刪除元素的值。
bool ListDelete(SeqList& L, int i, int e) {if (i<1 || i>L.length + 1)//判斷i的范圍是否有效return false;e = L.data[i - 1];for (int j = i; j < L.length; j++)L.data[j - 1] = L.data[j];L.length--;return true; }說明:刪除方法,有&L,同理,帶入L順序表進去操作后返回,最開始先檢查i的合理性,檢查成功后,將要刪除的值賦給e,然后開始把e這個i位置的元素往前移,把要刪除的元素擠掉,然后線性表長度減一,返回成功字樣。
關于插入操作的時間復雜度就不細說了,實際上和插入的時間復雜度一模一樣,計算方法也大同小異。
2.3.2.4 順序表的按位查找
GetElem(L, i):按位查找操作,獲取表L中第i個位置的元素的值。
int GetElem(SeqList L, int i) {//初始條件:順序線性表L已存在if (L.length == 0 || i<1 || i>L.length)return 0;return L.data[i - 1]; }實際上,動態數組也可以用這種方式訪問。
2.3.2.5 順序表的按值查找
LocateElem(L ,e ):按值查找操作。在表L中查找具有給定值的元素,并返回其所在的位序。
//按值查找 int LocateElem(SeqList L, int e) {for (int i = 0; i < L.length; i++)if (L.data[i] == e)return i + 1; //找到值,退出循環,返回位序return 0; //退出循環,說明查找失敗 }需要注意的是,當我們的順序表里面的元素不是基本類型而是結構類型的時候,按值查找的判定條件==就不能再用了。
對于結構體類型的數據元素,我們可以采用==來判定結構體中每個數據類型是否相等。最好的做法是能做成一個函數來使用。如果是C++的話,我們還可以對==進行重載。
不過幸運的是,在考研當中,目標學校更側重的是你對算法的理解,而不在代碼的細節。所以在手寫代碼的時候無論是基本數據類型還是結構數據類型都是可以直接使用==。
對于按值查找的時間復雜度來說,和插入操作一樣,稍加思考一下就能理解,這里就不過多講述了。
需要提到的是,按值查找也有技巧可言,并不一定要按照順序掃描的方式;比如二分查找等查找方法都能提高時間效率,在后面會有更深層次的講解。
總結
- 上一篇: Node.js Undocumented
- 下一篇: 计算机ws2_32dll丢失,电脑显示计