線性表的結構詳解

Dre丿發表於2019-04-14

一、線性表的定義

線性表(List):零個或多個元素的有限序列
需要注意的地方:

  1. 它是一個序列:也就是說,元素之間是有順序的,若元素存在多個,則第一個元素無前驅,最後一個元素無後繼,其他每個元素都有且只有一個前驅和後繼。
  2. 線性表強調是有限的,即元素的個數是有限的。
    線性表包含:順序表和連結串列,所以不管是哪一個都遵循線性表的這個約定。

二、線性表的儲存結構

1、順序儲存

線性表的順序儲存結構,指的是用一段地址連續的儲存單元依次儲存線性表的資料元素。因為底層實現原理是基於陣列實現,所以,在記憶體中的分配會開闢一段連續的地址空間,如圖所示:
Alt
具體的結構定義為:

#define MAXSIZE 20
typedef int ElemType;
typedef struct{
	ElemType data[MAXSIZE]; /*陣列儲存資料元素,最大為MAXSIZE*/
	int length /*線性表當前的長度*/
}SqList; 

陣列的長度”與“線性表的長度”區別:

  1. 陣列長度是存放線性表的儲存空間的長度,即能夠存放多少個資料值
  2. 線性表的長度即當前線性表中資料元素的個數
  3. 在任意時刻,線性表的長度應該小於等於陣列的長度。

1.1 線性表獲得元素的操作

/*獲取給定第i個元素位置,返回該位置的值*/
#define OK 1;
#define ERROR 0;
typedef int Status;
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;
}

1.2 插入操作

/*將元素e插入到順序表中的第i個位置*/
Status ListInsert(SqList *L,int i,ElemType e){
	int k;
	if(L->length == MAXSIZE){//容量已經達到上界 
		return ERROR;
	}
	if(i < 1 || i > L->length+1){//插入位置不合法 
		return ERROR;
	}
	if(i < L->length){//若插入的位置不在表尾 
		for(k = L->length-1;k >= i-1;k--){
			L->data[k+1] = L->data[k];//從第最後一個元素開始,依次向後挪一個位置,直到i的前一位 
		}
	}
	L->data[i-1] = e;//將元素插入第i個位置;
	L->length++ ; 
	return OK;
}

1.3 刪除操作

/*刪除順序表L中的第i個位置的值,並將其返回,length-1*/
SqList ListDelete(SqList *L ,int i,ElemType *e){
	int k;
	if(L->length == 0)
		return ERROR;
	if(i < 1 || i > L->length - 1){
		return ERROR;
	}
	*e = L->data[i-1];
	if(i < L->length -1){
		for(k = i;k < L->length;k++){
			L->data[k-1] = L->data[k];
		}
	}
	L->length --;
	return OK;
} 

1.4 插入與刪除的時間複雜度分析

先看 最好的情況: 如果插入最後一個位置,或者是刪除最後一個位置,時間複雜度為 O(1),此時不需要移動任何元素;最壞的情況: 如果插入和刪除都是第一個位置,需要將所有的元素都進行移動,時間複雜度為O(n)。 平均的情況: (0+1+…n-1)/n = (n-1)/2次,所以平均時間複雜度為O(n)。
說明線性表的順序儲存結構,在讀資料時,不管是哪個位置,時間複雜度均為O(1);而插入資料或刪除資料時,時間複雜度均為O(n)。

1.5 線性表的順序儲存結構的優缺點

     優點:		
		 1. 無須為表示表中元素之間的邏輯關係而增加額外的儲存空間
		 2. 可以快速儲存表中的任一元素			
     缺點:
		1. 插入和刪除操作需要移動的資料元素相對較高
		2. 當線性表的長度變化較大時,難以確定儲存空間的容量
		3.  造成儲存空間的“碎片”。			 

2、鏈式儲存

在引入鏈式儲存之前應該知道一個問題:為什麼會出現鏈式儲存的結構?
	個人的思考:一個新的東西出現肯定是在改進以前的方式存在的不足,從而讓效能得到最優化,提高效率。
	所以,引入鏈式儲存的原因也是在改進順序儲存結構的不足之處(上面已經介紹了順序儲存的不足),提升效能。

2.1鏈式儲存結構

線性表的鏈式儲存結構特點是用一組任意的儲存單元儲存線性表的資料元素,這組儲存單元可以是連續的,也可以不是連續的。 這一點就和順序表存在一的一種區別,即不需要再記憶體單元中分配連續的記憶體地址,在鏈式儲存中具體的結構,在下文會繼續深入。
以前在順序儲存結構中,每個資料元素只需儲存資料元素資訊就夠了。現在鏈式儲存結構中,除了要儲存資料元素的資訊之外,還要儲存後繼元素的儲存地址。
因此,為了表示每個資料元素aia_i與其直接後繼資料元素ai+1a_i+_1之間的邏輯關係,對元素aia_i來說,除了儲存本身的資訊之外,還需儲存一個指示其後繼的資訊(即直接後繼的儲存位置)。我們將資料元素的域稱為data域,將儲存直接後繼的位置稱為next指標域,指標域中儲存的資訊稱為指標或鏈。這兩部分組成了資料元素的儲存映像,稱為節點(Node)
具體的形式為:
在這裡插入圖片描述
連結串列中的第一個節點的儲存位置叫做頭指標,那麼整個連結串列的存取就是必須從頭指標開始進行。之後的每一個節點,其實就是上一個的後繼指標指向的位置。最後一個位置,由於後面沒有元素,因此其指向為NULL。
因此,在進行單連結串列的儲存時,會在第一個節點之前插入一個節點,稱為頭節點。頭節點也會存在兩個域,即data域指標域next,其中頭節點的data域可以不儲存資料,也可以用來儲存連結串列的長度等資訊,不是很重要。

2.2 頭指標與頭節點的區別

	**頭指標:**    
		1、頭指標是指連結串列指向第一個節點的指標,若連結串列有頭節點,則是指向頭節點的指標
		2、頭指標具有標識作用,常用頭指標稱為連結串列的名字
		3、無論連結串列是否為空,頭指標均不為空。頭指標是連結串列的必要元素(即必須要有)。			
	** 頭節點 **
        1、頭節點是為了操作的統一和方便而設立的,放在第一個元素的節點之前,其資料域一般無意義(也可以存放連結串列的長度)。
        2、有了頭節點在第一個元素之前的操作就與其他的節點操作就統一了。
        3、頭節點不一定是連結串列的必要元素(不是必須要有)。

2.3單連結串列的結構定義

/*線性表的儲存結構*/ 
typedef struct Node{
	ElemType data;
	struct Node * Next;
} Node;
typedef struct Node * LinkList;//定義單連結串列 

從結構定義可以看出,節點由存放資料的data域和存放後繼指標的next指標域組成。假設p是指向線性表第i個元素的指標,則節點aia_i的資料域我們可以用p->data來表示,表示當前節點的值,節點的指標域用p->next表示,其值是第i+1_i+_1個元素的地址。也就是說:p->data = aia_i,p->next->data = ai+1a_i+_1,具體如圖所示:
Alt

2.4 連結串列查資料

/* 用 e 返回L中第i個資料的元素*/
Status GetElem(Linklist L,int i,ElemType *e) {
	int k;
	LinkList p;   //定義一個指標p 
	p = L->next;  //將其指向第一個節點 
	k = 1;       //用於計數 
	while(p && k < i){
		p = p->next;
		++k;
	}
	if(!p || k> i){
		return ERROR;//第i個節點不存在
	}
	*e = p->data;
	return OK;
}

連結串列查資料就是從頭開始找,直到第i個節點為止。時間複雜度取決於i,當i = 1時,不需要遍歷,時間複雜度為O(1),當 i=n時,遍歷n-1此,最壞時間複雜度為O(n),

2.5 插入操作

單連結串列的插入可以理解為:
Alt
將元素e插入到連結串列中,實現為:

s->next = p->next; p->next = s ; 

具體的實現演算法如下:

/*向單連結串列的第i個位置插入一個新的節點e*/
Status ListInsert(LinkList *L,int i,ElemType *e){
	int k = 1;//標記位 
	LinkList p,s;
	p = *L;
	while(p && k < i){//尋找第i-1個位置 
		p = p->next;
		k++;
	}
	if(!p || k > i){// 第i個節點不存在 
		return ERROR;
	}	
	s = (LinkList) malloc(sizeof(Node));//生成一個型別位LinkList,大小為Node的節點s
	s->data = e; 
	s->next = p->next;//將p的後繼連結在s後面 
	p->next = s;//將s連結在p後面 
	return OK;
}

2.6 刪除操作

單連結串列的刪除操作可以理解為:
Alt
實現單連結串列的刪除操作,其實就是將ai1a_i-_1的next指向ai+1a_i+_1,實現為:

q = p->next;  p->next = q->next;
delete(q); //將記憶體中的q節點釋放掉,即釋放掉要刪除的元素記憶體

具體的實現程式碼如下:

/*刪除連結串列L的第i個節點,並將其值返回到e,長度減1*/
Status ListDelete(LinkList *L,int i,ElemType *e){
	
	int k = 1;
	LinkList p,q;
	p = *L;
	while(p->next && k < i){
		p = p->next;
		k++;
	}
	if(!(p->next) || k > i){
		return ERROR;
	}
	q = p->next;//將q指向p的後繼 
	p->next = q->next; //將p的後繼指向q的後繼 
	*e = q->data;
	delete(q);//釋放節點q 
	L->length--;
}

從整個演算法來看,很容易推出,他們的時間複雜度都為O(n),如果不知道單連結串列第i個節點的位置,單連結串列的插入刪除操作,與線性表的順序儲存結構是沒有區別的,但是如果希望從第i個位置,插入節點,則在順序表中的時間複雜度為O(n),需要將n-i個資料不斷的進行移動,但是在單連結串列中,進行插入刪除操作時,只需要改變連結串列的指標指向位置,時間複雜度為O(1)。因此,對於插入和刪除操作很頻繁的時候,選用連結串列的結構時比較方便高效的

2.7 單連結串列的整表建立

順序表的建立中,其實就是陣列的初始化操作,即宣告一個型別和大小的陣列並賦值的過程。但是在單連結串列中,它不像順序結構這麼集中,它可以很分散,是一種動態結構。對於連結串列來說,它所佔用的空間大小和位置不需要預先分配好,可以根據實際需求進行即時生成

2.7.1 頭插法

單連結串列建立整表頭插法演算法思路:

/*
	隨機產生n個值,並將其插入到單連結串列頭部
*/
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;
		p->next = (*L)->next;
		(*L)->next = p;    //插入到表頭 
	} 
}

2.7.2 尾插法

單連結串列建立整表尾插法演算法思路:

/*
	隨機產生n個值,並將其插入到單連結串列尾部
*/
void CreateListTail(LinkList *L,int n){
	LinkList p,r;
	int i;
	srand(time(0))  //初始化 
	*L = (LinkList) malloc(sizeof(Node));
	(*L)->next = NULL;
	r = *L;//將r指向表尾 
	for( i = 0;i < n;i++){
		p = (LinkList) malloc(sizeof(Node));  //生成新的節點 
		p->data = rand()%100 +1;
		r->next = p;//將一個新的節點插入到表尾 
		r = p;    //移動尾節點的指標到最後p節點位置 
	} 
	r->next = NULL; 
}

2.8 單連結串列的整表刪除

當不需要單連結串列時,則對其進行刪除操作,其實就是將它在記憶體中的空間釋放掉。

/*將單連結串列中的元素逐個刪除,清空單連結串列*/
Status ClearList(LinkList *L){
	LinkList p,q;
	p = (*L)->next;
	while(p){
		q = p->next;
		delete(p);
		p = q;
	}
	(*L)->next = NULL;//將頭節點的指標域置為空 
	return OK; 
}

在該程式碼中,應該思考一個問題,q的作用:在進行刪除操作時,如果刪除當前節點,為了下次能尋找到下一個節點在哪,所以必須在進行刪除之前,將當前節點的下一個節點儲存起來。

2.9 單連結串列結構與順序表儲存結構優缺點

單連結串列結構順序儲存結構的對比:

  1. 儲存分配方式
    • 順序儲存結構用一段連續的地址空間進行儲存。
    • 單連結串列採用鏈式儲存結構,用一組任意的儲存單元存放線性表的元素。
  2. 時間效能:
    1、查詢: 順序儲存結構O(1) 鏈式儲存結構O(n)
    2、插入和刪除順序儲存結構 需要平均移動表長一半的元素,時間為O(n);單連結串列 的插入和刪除僅為O(1)。
  3. 空間效能
    順序儲存結構需要預分配儲存空間,不好確定大小,所以一般都會預分配相對較大的空間;
    單連結串列不需要分配儲存空間,只要記憶體有空間就可以分配,元素個數也不受限制。

三、迴圈連結串列

定義: 將單連結串列中終端節點的指標由空指標改為指向頭節點,就使整個單連結串列形成一個環,這種頭尾相接的單連結串列稱為單迴圈連結串列,簡稱迴圈連結串列
對於非空迴圈單連結串列結構示意圖可以表示為:
Alt

  • 其實迴圈連結串列和單連結串列的主要差異在於迴圈的判斷條件上,單連結串列判斷 p->next = NULL,迴圈連結串列是判斷p->next是否為頭節點,則迴圈結束。
  • 在單連結串列中有了頭節點,可以用O(1)時間訪問第一個節點,但對於要訪問最後一個結點,卻需要O(n)時間將整個連結串列訪問一遍。

如果要在時間O(1)內訪問連結串列的尾部結點,可以在連結串列的尾部設定一個尾部指標rear,則其頭部指標可以表示為rear->next。
舉個例子:將兩個迴圈連結串列rearA、rearB合成一個表,示意圖為:
Alt
操作步驟:
Alt
實現的過程:

p = rearA->next;  //操作 ①儲存A表的頭節點 
rearA->next = rearB->next->next; //操作② 將B錶連結在A表後面 
q = rearB->next;
rearB->next = p;//操作 3 講A表的頭節點賦給rearB->next 
delete(q);//刪除B表的頭節點 

四、雙向連結串列

為了克服單項性的缺點,設計了雙向連結串列,雙向連結串列是在單連結串列的每個結點中,再設定一個指向其前驅結點的指標域,所以在雙線連結串列中,每個結點都有兩個域:前驅指標後繼指標
雙向連結串列的結構定義為:

typedef DulNode{
	ElemType data;
	stauct  DulNode *prior; 
	stauct  DulNode *next;
}DulNode,*DuLinkList;

結構示意圖為:
Alt
因此,在雙向連結串列中,p->next->prior = p = p->prior->next
雙向連結串列的插入操作:
Alt
實現細節:

s->prior = p; //將s的前驅指向p
s->next = p->next;//將s的後繼指向p的後繼
p->next->prior = s;//將p的後繼的前驅指向s
p->next = s;//將p的後繼指向s

雙向連結串列的刪除操作:
Alt
實現細節:

p->prior->next = p->next;
p->next->prior = p->prior;

相關文章