數據結構之線性表【C語言】
參考書籍:大話數據結構
文章目錄
舉個“栗子”
今天我們介紹數據結構中最常用和最簡單的一種結構,在介紹它之前先講個例子。
假如我有個兒子,我經常下午去幼兒園接送兒子,每次都能在門口石到老師帶着小朋友們,一個拉着另一個的衣服,依次從教室出來。而且我發現很有規律的是,每次他們的次序都是一樣。比如我兒子排在第5 個,每次他都是在第5 個,前面同樣是那個小女孩,後面一直是那個小男孩。這點讓我很奇怪,爲什麼一定要這樣?
有一天我就問老師原因。她告訴我,爲了保護小朋友的安全,避免漏掉小朋友,所以給他們安排了出門的次序,不先規定好了,誰在誰的前面,誰在誰的後面。這樣養成習慣後,如果有誰沒有到位,他前面和後面的小朋友就會主動報告老師,某人不在。即使以後如果要外出到公園或博物館等情況下,老師也可以很快地清點人數,萬一有人走丟,也能在最快時間知道,及時去尋找。
我一想,還真是這樣。小朋友們始終按照次序排隊做事,出意外的情況就可能會少很多。畢竟,遵守秩序是文明的標誌,應該從娃娃抓起。而且,真要有人丟失,小孩子反而是最認真負責的監督員。
再看看門外的這幫家長們,都擠在大門口,哪個分得清他們誰是誰呀。與小孩子們的井然有序形成了鮮明的對比。哎,有時大人的所作所爲,其實還不如孩子。這種排好隊的組織方式,其實就是今天我們要介紹的數據結構線性表.
線性表的定義
線性表,從名字上你就能感覺到,是具有像線一樣的性質的表。在廣場上,有很多入分散在各處,當中有些是小朋友,可也有很多大人,甚至還有不少寵物,這些小朋友的數據對於整個廣場人羣來說,不能算是線性表的結構。但像剛纔提到的那樣,一個班級的小朋友,有一個打頭,有一個收尾,當中的小朋友每一個都知道他前面一個是誰,他後面一個是準,這樣如同有一根線把他們串聯起來了。就可以稱之爲線性表。
這裏需要強調幾個關鍵的地方。
- 首先它是一個序列. 也就是說,元素之間是有順序的,若元素存在多個,則第一個元素無前驅,最後一個元素無後繼,其他每個元素都有且只有一個前驅和後繼。如果一個小朋友去拉兩個小朋友後面的衣服,那就不可以排成一隊了,同樣,如果一個小朋友後面的衣服,被兩個甚至多個小朋友拉扯,這其實是在打架,而不是有序排隊.
- 然後,線性表強調是有限的,小朋友班級人數是有限的, 元素個數當然也是有限的。事實上,在計算機中處理的對象都是有限的,那種無限的數列,只存在於數學的概念中。
- 元素的類型要是同一類型的
線性表元素的個數n (n > O ) 定義爲線性表的長度,當n=O 時,稱爲空表。
在非空表中的每個數據元素都有一個確定的位置,如a , 是第一個數據元素, an 足最後一個數據元素, ai 是第l 個數據元素,稱i 爲數據元素a i 在線性表中的位序。
線性表的抽象數據類型
前面我們巳經給了線性表的定義,現在我們來分析一下,線性表應該有一些什麼樣的操作呢?
還是回到剛纔幼兒園小朋友的例子,老師爲了讓小朋友有秩序地出入, 所以就爲慮給他們排一個隊,並且是長期使用的順序,這個考慮和安排的過程其實就是一個線性表的創建和初始化過程.
一開始沒經驗,把小朋友排好隊後,發現有的高有的矮, 於是就讓小朋友解散重新排~是一個線性表重置爲空表的操作.
排好了隊,我們隨時可以叫出隊伍某一位置的小朋友名字及他的具體情況. 比如有家長問,隊伍裏第五個孩子, 怎麼這麼調皮,他叫什麼名字呀,老師可以很快告訴這位家長,這就是封清揚的兒子,叫封雲卞. 我在旁就非常扭捏,石來是我給兒子的名字沒取好,兒子讓班級.風雲突變.了. 這種可以根據位序得到數據元素也是一種
很重要的線性表操作.
還有什麼呢,有時我們想知道,某個小朋友, 比如麥兜是否是班裏的小朋友,老師會告訴我說,不是,麥兜在春田花花幼兒園裏,不在我們幼兒園。這種查找某個元素是否存在的操作很常用.
而後有家長問老師,班裏現在到底有多少個小朋友呀,這種獲得線性表長度的問題也很普遍。
顯然,對於一個幼兒園來說,加入一個新的小朋友到隊列中,或因某個小朋友生病,需要移除某個位置,都是很正常的情況.對於一個線性表來說,插人數據和刪除數據都是必須的操作.
所以,線性表的抽象數據類型定義如下
ADT 線性表
Data
線性表的數據對象集合爲(a1,a2,...., an)每個元素的類型均爲DataType。其中,除第一個元素a1外,每個元素有且僅有一個直接前驅元素,除了最後一個元素an外,每個元素都有且僅有一個直接後繼元素。數據元素之間的關係是一對一的關係
Operation(基本的線性表操作):
InitList (*L); 初始化操縱,建立一個空的線性表L
ListEmpty(L); 若線性表爲空,返回true,否則返回false
ClearList(*L); 將線性表清空
GetElem(L,i,*e):將線性表L中的第i個位置的元素值返回給e
LocateElem(L,e): 在線性表L中查找與給定值e相等的元素,如果查找成功,返回該元素在表中序號表示成功,否則返回0表示失敗
ListInsert(*L,i,e): 在線性表L中的第i個位置插入新元素e
ListDelete(*L,i,*e): 刪除線性表L中第i個位置元素,並用e返回其值
ListLength(L): 返回線性表L的元素的個數
上面列出了線性表的基本操作下面會對各個操作一一實現的
對於不同的應用,線性表的基本操作是不同的, 上述操作是最基本的,對於實際問題中涉及的關於線性表的更復雜操作,完全可以用這些基本操作的組合來實現.
比如,要實現兩個線性表集合A 和B 的並集操作. 即要使得集合A=A U B . 說白了,就是把存在焦合B 中但並不存在A 中的數據元素插入到A 中即可。
線性表的順序存儲結構
順序結構的定義
說了那麼多的線性表,我們來看看線性表的兩種物理結構的第一種——順序存儲結構。
線性表的順序存儲結構,指的是用一段地址連續的存儲單元一次存儲的線性表的數據元素
示意表如下:
順序表的存儲方式
線性表的順序存儲結構,說白了,和剛纔的例子一樣,就是在內存中找了塊地兒,通過佔位的形式,把一定內存空間給佔了,然後把相同數據類型的數據元素依次存放在這塊空地中. 既然線性表的每個數據元素的類型都相同,所以可以用C 語言(其他語言也相同)的一維數組來實現順序存儲結構, 即把第一個數據元素存到數組下標爲0 的位置中, 接右把線性表相鄰的元素存儲在數組中相鄰的位置。建立一個線性表, 要在內存中找一塊
地,於是這塊地的第一個位置就非常關鍵, 它是存儲空間的起始位置.
你可以把線性表的創建比作在圖書館佔位置,這樣的模型聯繫更加有助於你去理解線性表,佔座時,如果圖書館裏空座很多,你當然不必一定要選擇第一排第一個位子,而是可以選擇風水不錯、美女較多的地兒。找到後,放一個書包在第一個位,就表示從這開始, 這地方暫時歸我了。接若,因爲我們一共九個人,所以我需要佔九個座。線性表中,我們估算這個線性表的最大存儲容貸,建立一個數組,數組的長度就是這個最大存儲容員.
可現實中, 我們宿舍總有那麼幾個不是很好學的人, 爲了遊戲,爲了戀愛, 就不去圖書館自習了。假設我們九個人,去了六個,真正被使用的座位也就只是六個, 另三個是空的。同樣的,我們已經有了起始的位置,也有了最大的容憂, 於是我們可以在裏面增加數據了。隨行數據的插入, 我們線性表的長度開始變大,不過線性表的當前長度不能超過存儲容優,即數組的長度。想想也是,如果我們有十個人,只佔了九個座,自然是坐不下的。
這一段的例子是從大話數據結構這本書上引用的,我認爲這種比喻方式非常的形象。將建立數組比作在大學圖書館佔位置。
Code
#define MAXSIZE 20 // 存儲空間初始分配量
typedef int ElemType // ElemType 類型根據實際情況而定,這裏假設爲int * /
typedef struct{
ElemType data[MAXSIZE]; // 數組存儲的數據元素,最大值爲MAXSIZE
int length; // 線性表當前的長度
}SqList;
這裏,我們就發現描述順序存儲結構需要三個屬性
- 存儲空間的起始位置: 數組data,它的存儲位置就是存儲空間的存儲位置。
- 線性表的最大儲存容量:數組長度MaxSize
- 線性表的當前長度length
數據長度和線性表長度區別
數組的長度是存放線性農的存儲空間的長度,存儲分配後這個員是一般是不變的。一般在級語言,比如C, VB 、C+ +都可以用編程手段實現動態分配數組,不過這會帶來性能上的損耗。
線性表的長度是線性表中數據元素的個數,隨着線性表插入和刪除操作的進行,這個量是變化的。在任意時刻, 線性表的長度應該小於等於數組的長度。
地址計算方法
由於我們數數都是從1 開始數的,線性表的定義也不能免俗,起始也是1 , 可C語言中的數組卻是從0 開始第一個下標的,於是線性表的第i 個元素是要存儲在數組下標爲,- 1 的位置,即數據元素的序號和存放它的數組下標之間存在對應關係,如下圖所示:
其實,內存中的地址,就和圖書館或電影院裏的座位一樣, 都是有編號的。存儲器中的每個存儲單元都有自己的編號,這個編號稱爲地址,由於每個數據元素,不管它是整型、實型還是字符型,它都是需要佔用一定的存儲單元空間的。假設佔用的是C 個存儲單元,那麼線性表中第i +1 個數據元素的存儲位置和第i 個數據元素的存儲位置滿足下列關係(LOC 表示獲得存儲位置的函數)
所以對於第i個數據元素ai的存儲位置可以由a1推算出:
通過這個公式,你可以隨時扛出線性表中任意位置的地址,不管它是第一個還是最後一個,都是相同的時間。那麼我們對每個線性表位置的存人或者取出數據,對於計算機來說都是相等的時間,也就是一個常數,因此用我們絆法中學到的時間複雜度的概念來說,它的存取時間性能爲0(1 ) 。我們通常把具有這一特點的存儲結構稱爲隨
機存取結構。
順序結構的插入與刪除
獲取元素操作:
對於線性表的順序存儲結構來說,如果我們要實現GetElem操作, 即將線性表L中的第 i 個位置元素值返回,其實是非常簡單的。就程序而官,只要 i 的數值在數組下標範圍內,就是把數組第i -1 下標的值返回即可。來行代碼
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
// Status是函數類型,其值是函數結果狀態代碼,如OK等
// 初始條件:順序線性表L已經存在, i <= i <= ListLength(L)
// 操作結果:用e返回 L中的第i個元素的值
Status GetElem(SqList L,int i,ElemType *e)
{
if(L.length == 0 || i<1 || i>L.length)
return ERROR;
*e = L.data[i-1];
return OK
}
插入操作
剛纔我們也談到,這裏的時間複雜度爲0(1) 。我們現在來考慮,如果我們要實現Listlnsert ( *L,i,e ), 即在線性表L 中的第i 個位置插人新元素e , 應該如何操作?
- 如果插入位置不合理,拋出異常
- 如果線性表長度大於數組長度,則拋出異常或動態容量增加
- 從最後一個元素開始向前遍歷第i個位置,分別將它們都先後移動一個位置;
- 將要插入元素填在位置i處;
實現代碼如下
//---insert into the SqList---
Status ListInsert(SqList *L,int i,ElemType e)
{
int k;
if(L->length == MAXSIZE) // SqList is full
return ERROR;
if(i<1 || i>L->length+1) // when i is not in the right range
return ERROR;
if(i<=L->length) // If the inserted content is not at the end of the list
{
for(k=L->length-1; k=i-1; k--)
{
L->data[k+1] = L->data[k];
}
}
L->data[i-1] = e; // insert the new number
L->length ++;
return OK;
}
刪除操作
刪除算法的思路:
- 如果刪除位置不合理,拋出異常
- 取出刪除元素
- 從刪除位置開始遍歷到最後一個元素位置,分別將它們都先後移動一個位置
- 表長減一
Code
//---delete the number in the SqList---
Status ListDelete(SqList *L,int i,ElemType *e)
{
int k;
if(L->length==0) // SqList is empty
return ERROR;
if(i<1 || i>L->length) // the location of the delete is error
{
return ERROR;
}
*e = L->data[i-1];
if (i<L->length)
{
for(k=i; k<L->length; k++) // Moves the element forward after the deleted number's position
L->data[k-1] = L->data[k];
}
L->length--;
return OK;
}
線性表順序存儲結構的優缺點
線性表的鏈式存儲結構
這裏我們只討論,動態鏈表的相關操作;
鏈表的創建和初始化
其實C 語言真是好東西,它具有的指針能力,使得它可以非常容易地操作內存中的地址和數據,這比其他高級語官更加靈活方便。後來的面嚮對象語言,如Java 、C#等,雖不使用指針,但因爲啓用了對象引用機制,從某種角度也間接實現了指針的某些作用。
對於鏈表的創建和初始化,其實就是對於頭節點的創建,這個頭節點是無論如何也不能丟失的
,下面我們把他封裝成一個函數
首先先簡單的創建一個節點結構體
struct Node{
int number;
struct Node *next;
};
typedef struct Node Node;
接下來我們寫一個createNode()的函數用來創建頭節點,也是對鏈表的初始化
// create node
Node *createNode() {
Node *node = (Node *) malloc(sizeof(Node));
node->next = NULL;
return node;
}
鏈表的插入操作
尾部插入
尾插的時候你需要先找到那個尾巴纔行,也就是需要一個遍歷的過程,我們也把他封裝成一個函數
void tailInsert(Node *head, int number) {
// init tail
Node *tail = createNode();
tail = head;
// find the tail
while (tail->next) {
tail = tail->next;
}
// insert the number
Node *temp = createNode();
temp->next = NULL;
temp->number = number;
tail->next = temp;
}
這裏我畫了一個示意圖可以參考一下:
頭部插入
頭插法的特點就是從頭部插入,同樣的我們把它封裝成一個函數。
Node *headInsert(Node *head,int number)
{
Node *temp = createNode();
temp->number = number;
temp->next = head->next;
head->next = temp;
return head;
}
示意圖:
中間插入
我這裏因爲結構體設置的比較簡單所以就是用number來作爲插入的標誌,在別的結構體當中你可以把它換成其他的
void middleInsert(Node *head, int num,int number)
{
Node *p = createNode();
for (p=head->next;p!=NULL;p=p->next) {
if (p->number == num)
{
Node *temp = createNode();
temp->number = number;
temp->next = p->next;
p->next = temp;
break;
}
}
}
示意圖:
鏈表的遍歷操作
對於鏈表遍歷的基本思想就是,看一看p->next
的是不是NULL了,以這個作爲一個標誌。因爲頭節點我們是不可以動的,所以我們需要多一個指針指向頭節點來代替頭節點去完成這個遍歷的操作,你可以把一般名爲爲p,tail也行
我們把這個操作也封裝成一個函數
void traverseLink(Node *head)
{
Node *p = createNode();
p = head;
for(p=head->next;p!=NULL;p=p->next)
{
printf("%d\n",p->number);
}
}
鏈表的刪除操作
想要刪除鏈表中的一個元素很簡單,就是讓上一個元素的指針跳過中間那個直接指向下一個就ok啦。
void deleteNode(Node *head,int number)
{
Node *p = createNode();
for (p=head->next;p->next !=NULL;p=p->next) {
if (p->next->number == number)
{
p->next = p->next->next;
}
}
}