資料結構-線性表
線性結構:線性結構中的元素之間的關係是一對一的關係。
線性表(List):零個或多個數據元素的有限序列。而資料元素可以是由若干個資料項組成。
線性表常用的操作:初始化InitList(建立一個空表)、判斷線性表是否為空ListEmpty、清空線性表ClearList、返回線性表第i個位置元素GetElem、查詢線性表與給定值相同的元素LocateElem、插入ListInsert、刪除ListDelete、長度ListLenngth。
線性表的順序儲存結構:指的是用一段地址連續的儲存單元依次儲存線性表的資料元素。
線性表的順序儲存結構程式碼:
typedef int ElemType; const int MAXSIZE = 20; typedef struct { ElemType data[MAXSIZE]; int length; }SqList;
描述順序儲存結構的三個屬性:
(1)儲存空間的起始位置:陣列data。
(2)線性表最大的儲存容量:MAXSIZE。
(3)線性表當前的長度:length。
順序儲存的特性,所以可以計算出線性表中的任意位置的地址, 那麼我們對每個線性表位置的存入或者取出資料,對計算機都是相等時間,存取時間效能為O(1)。通常把具有這一特點的儲存結構稱為隨機存取結構。
線性表的順序儲存結構-插入演算法的思路:
(1)如果插入位置不合理,丟擲異常;
(2)如果線性表長度大於等於陣列長度(或者說最大長度),則丟擲異常或者動態增加容量;
(3)從最後一個元素開始向前遍歷到第i個位置,分別將它們都向後移動一個位置。
(4)將要插入元素填入i處;
(5)線性表長度加1。
程式碼如下:
int ListInsert(SqList *L,int i,ElemType e) { if(L->length == MAXSIZE) return FALSE; if(i<1 || i>L->length+1) return FALSE; if(i <= L->length) //若插入資料位置不在表尾 { for(int k = L->length-1;k >= i-1;k--) { L->data[k+1] = L->data[k]; } } L->data[i-1] = e; L->length++=; return TRUE; }
線性表的順序儲存結構-刪除演算法的思路:
(1)如果刪除位置不合理,丟擲異常;
(2)取出刪除元素;
(3)從刪除元素位置開始遍歷到最後一個元素位置,分別將它們都向前移動一個位置;
(4)線性表長度減1。
bool ListDelete(Sqlist *L,int i,ElemType *e)
{
if(L->length == 0)
return FALSE;
if(i<1 || i>L->length)
return FALSE;
*e = L->data[i-1];
for (int k = i-1;k <= length-1;k++)
{
L->data[k] = L->data[k+1];
}
L->length--;
return TRUE;
}
線性表順序儲存結構的插入和刪除操作的時間複雜度都為O(n)。
線性表順序儲存結構的優點和缺點:
優點:(1)無需為表示元素之間的邏輯關係而增加額外的儲存空間
(2)可以快速地存取表中任一位置的元素
缺點:(1)插入和刪除操作需要移動大量元素
(2)當線性表長度變化較大時,難以確定儲存空間的容量
(3)造成儲存空間的碎片(由於線性表長度有時小於最大容量)
順序儲存結構插入和刪除速度慢的原因:由於資料在記憶體中儲存的形式是連續的,但正由於這種連續性,所以存取資料很快。
為了解決順序儲存不足:用線性表另外一種結構-鏈式儲存。
線性表的鏈式儲存結構的特點是用一組任意的儲存單元儲存線性表的資料元素,這組儲存單元可以是連續的,也可以是不連續的。在順序結構中,每個資料元素只需要存資料元素資訊就行了,而在鏈式結構中,除了儲存資料元素資訊外,還要儲存它的後繼元素的儲存地址。所以一般結點包括兩個資訊:資料和指標。連結串列就是n個節點組成的,如何每個結點只包含一個指標,那麼就是單鏈表。
有頭有尾:我們把連結串列中第一個結點的儲存位置叫作頭指標,那麼整個連結串列的存取就必須是從頭指標開始進行的。而線性連結串列的最後一個結點指標為空(NULL)。
有時,為了更方便對連結串列進行操作,會在單鏈表的第一個結點前加一個頭結點。頭結點的資料域可以不儲存任何資訊,也可以儲存如線性表長度等附加資訊,頭結點的指標域儲存指向第一個結點的指標。
(ps:大話資料結構-P58頁,圖3-6-4,3-6-6有誤,當初理解這裡花了好長時間,圖文表現的不是一個意思,無語!)
頭指標和頭結點的異同
頭指標
(1)指連結串列指向第一個結點的指標,若連結串列有頭結點,則是指向頭結點的指標。
(2)頭指標具有標識作用,所以常用頭指標冠以連結串列的名字。
(3)無論連結串列是否為空,頭指標均不為空。頭指標是連結串列的必要元素。
頭結點 (1)頭結點是為了操作的統一和方便而設立的,放在第一個元素的結點之前,其資料域一般無意義(也可存放連結串列的長度)
(2)有了頭結點,對在第一元素結點前插入結點和刪除第一結點,其操作與其它結點的操作就統一了。
(3)頭結點不一定是連結串列必須的要素。
//線性表的單鏈表儲存結構
typedef struct Node
{
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList;
單鏈表的讀取
獲取連結串列第i個數據的演算法思路:
(1)宣告一個指標p指向連結串列的第一個結點,初始化j從1開始;
(2)當j<i時,就遍歷連結串列,讓p的指標向後移動,不斷指向下一結點,j累加1;
(3)若到連結串列末尾p為空,則說明第i個結點不存在;
(4)否則查詢成功,返回結點p的資料。
//初始條件:連結串列L已存在,且1≤i≤ListLength(L)
//操作結果:用e返回L中第i個數據元素的值
bool GetElem(LinkList L,int i,ElemType *e)
{
int j = 1;
LinkList p;
p = L->next; //指向第一個結點,這裡是有頭結點的情況
while( p && j<i )
{
p = p -> next;
++j;
}
if( !p || j>i )
{
return FALSE;
}
*e = p->data;
return TRUE;
}
單鏈表的讀取時間複雜度是O(n)。
單鏈表的插入和刪除
若將結點插入結點p和p->next結點之間,只需要做如下變換:
s->next = p->next;
p->next = s;
這兩句的順序是不能改變的。
單鏈表第i個數據插入結點的演算法思路是:
(1)宣告一個指標p指向連結串列的第一個結點,初始化j從1開始; (2)當j< i 時,遍歷連結串列,讓p的指標向後移動,不斷指向下一結點,j累加1; (3)若到連結串列末尾p為空,說明第i個元素不存在; (4)否則查詢成功,在系統中生成一個空結點s; (5)將資料元素e賦值給s->data; (6)單鏈表的插入標準語句 s->next = p->next; p->next = s; (7)返回成功。
/*初始條件:順序線性表L已存在,1≤i≤ListLength(L)*/
/*操作結果:在L中第i個位置之前插入新的資料元素e,L的長度加1*/
bool ListInsert(LinkList *L,int i ,ElemType e)
{
int j;
LintList p,s;
p = *L;
j = 1;
while (p && j<i) /*尋找第i個結點*/
{
p = p->next;
++j;
}
if (!p || j >i)
return FALSE; /*第i個元素不存在*/
s = (LinkList)malloc(sizeof(Node)); /*生成新的結點*/
s->data = e;
s->next = p->next; /*將p的後繼結點賦值給s的後繼*/
p->next = s; /*將s賦值給p的後繼*/
return TRUE;
}
單鏈表的刪除
實際上就是一步,p->next = p->next->next. 用p來取代p->next;
q=p->next;
p->next = q->next;
單鏈表第i個數據刪除結點的演算法思路:
(1)宣告一個指標p指向連結串列的第一個結點,初始化j從1開始; (2)當j< i 時,遍歷連結串列,讓p的指標向後移動,不斷指向下一結點,j累加1; (3)若到連結串列末尾p為空,說明第i個元素不存在; (4)否則查詢成功,將欲刪除的結點p-next賦值給q; (5)單鏈表的刪除標準語句 p->next = q->next; (6)將q結點中的資料賦值給e,作為返回; (7)釋放q結點; (8)返回成功。
bool ListDelete(LinkList *L,int i ,ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next && j <i)
{
p = p->next;
++j;
}
if(!(p->next) || j>i)
return FALSE;
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return TRUE;
}
對於基本的插入與刪除操作,它們其實都是兩部分組成:
1.遍歷查詢第i個結點; 2.插入和刪除結點。
從整個演算法中,時間複雜度都是O(n)。如果在我們不知道第i個結點的指標位置,單鏈表資料結構在插入和刪除操作上,與線性表的儲存結構沒有太大的優勢。但是如果我們希望從第i個位置,插入10個結點,對於順序儲存結構來說,每一次插入都需要移動n-i個結點,每次都是O(n)。而單鏈表,我們只需要在第一次時,找到第i個位置的指標,此時為O(n),接下來只是簡單通過賦值移動指標而已,時間複雜度都是O(1)。顯然,對於插入和刪除資料越頻繁的操作,單鏈表效率優勢越明顯。
單鏈表的整表建立
單鏈表是一種動態結構。 對於每個連結串列來說,它所佔用的空間大小和位置是不需要預先分配的。 所以建立單鏈表的過程,就是從“空表”的初始狀態,一次建立各元素結點,並插入連結串列中、
單鏈表的整表建立的演算法思路: (頭插法) 1.宣告以指標p和計數器變數i; 2.建立一個空連結串列L; 3.讓L的頭結點的指標指向NULL; 4.迴圈:
生成以新結點賦值給p 隨機生成以數字賦值給p->data; 將p插入到頭結點與前一新結點之間。頭插法,示意圖:
/*隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(頭插法)*/
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); /*當前時間種下隨機數種子*/
*L = (LinkList)malloc(sizeof(Node));
(*L) ->next = NULL; /*建立帶頭結點的連結串列*/
for (i=0,i<n,i++)
{
P = (LinkList)malloc(sizeof(Node)); /*生成新結點*/
p->data = rand()%100+1; /*隨機生成100以內的數字*/
p->next = (*L)->next;
(*L)->next = p; /*插入到表頭*/
}
}
尾插法:把每次新結點都插在終端結點的後面
/*隨機產生n個元素的值,建立帶表頭結點的單鏈線性表(尾插法)*/
void CreateListTail(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node))
r =*L /*r為指向尾部的結點*/
for(i=0;i<n;i++)
{
p = (Node *)malloc(sizeof(Node)) /*生成新結點*/
p->data = rand() %100 +1;
r->next = p; /*將表尾終端結點的指標指向新結點*/
r = p; /*將當前的新結點定義為表尾終端結點*/
}
r->next = NULL; /*表示當前連結串列結束*/
}
單鏈表的整表刪除
當我們不打算使用這個單鏈表時候,我們需要把它銷燬,其實就是在記憶體中將它釋放掉。
思路: 1.宣告結點p和q; 2.將第一個結點賦值給p; 3.迴圈:
- 將下一結點賦值給q;
- 釋放p;
/*初始條件:順序線性表L已存在,操作結果:將L重置為空表*/
Status ClearList(LinkList *L)
{
LinkList p,q;
p = (*L)->next; /*p指向第一個結點*/
while(p) /*沒到表尾*/
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; /*頭結點指標域為空*/
return OK;
}
6.總結 單鏈表結構與順序儲存結構優缺點 時間效能 順序儲存結構: 查詢為O(1),因其是隨機存取結構;插入與刪除需要平均移動表長一半的元素,故為O(n);
單鏈表:查詢為O(n),查詢演算法的時間複雜度取決於i的位置,當i=1時,則不需要遍歷,第一個就取出資料了,而當i=n時則遍歷n-1次才可以。因此最壞情況為O(n);單鏈表在確定出某位置的指標後,插入和刪除時間僅為O(1);
空間效能 順序儲存結構:需要預分配儲存空間;
單鏈表:不需要分配儲存空間,只要有就可以分配,元素個數也不受限制。
若線性表需要頻繁查詢,很少進行插入與刪除操作時,宜採用順序儲存結構;若需要頻繁插入和刪除時,宜採用單鏈表結構。 當線性表中的元素個數變化較大或未知時,最好使用單鏈表。如果實現知道線性表的大致長度則使用順序儲存結構效率會高很多。