線性表
什么是線性表?
線性表(List):由零個或多個數據元素組成的有限序列
-
- 首先它是一個序列
- 若元素存在多個,則第一個元素無前驅,而最后一個元素無后繼,其他元素有且職友一個前驅和后繼
- 線性表強調是有限的
- 線性表的個數n(n>=0)定義為線性表的長度,當n=0時,稱為空表
抽象數據類型
數據類型:是指一組性質相同的值的集合及定義在此集合上的一些操作的總稱
例如在C語言中按照取值不同,數據類型可以分為兩類:
原子類型:不可以再分解的基本類型,例如整型,浮點型,字符型
結構類型:由若干個類型組合而成,是可以再分解的,例如整型數組
抽象:是指由取出事物具有的普遍性的本質,它要求抽出問題的特征而忽略非本質的細節,是對具體事務的一個概括
抽象數據類型:是指一個數學模型及定義在該模型上的一組操作
-
- 我們對已有的數據類型進行抽象,就有了抽象數據類型
- 抽象數據類型的定義僅取決於它的一組邏輯特性,而與其在計算機內部如何表示和實現無關
- 例如我們定義一個坐標Vector3由x、y、z三個整型數據組合,那么Vector3就是一個抽象數據類型
描述抽象數據類型的標准格式:
ADT 抽象數據類型名
Data
數據元素之間邏輯關系的定義
Operation
操作
endADT
線性表類型定義
ADT 線性表(List) Data 線性表的數據對象集合為{a1,a2,...,an},每個元素的類型均為DataType Operation InitList(*L):建立一個空的線性表L ListEmpty(L):判斷線性表是否為空表,為空返回true,不為空返回false ClearList(*L):將線性表清空 GetElem(L,i,*e):將線性表L中的第i個位置元素值返回給e LocateElem(L,e):在線性表L中查找與給定值e相等的元素,成功返回true,失敗返回false ListInset(*L,i,e):在線性表L第i個位置插入元素e ListDelete(*L,i,*e):刪除線性表L中第i個位置的元素並用e返回其值 ListLength(L):返回線性表L的元素個數 endADT
線性表的順序存儲結構
線性表由兩種物理存儲結構:順序存儲結構和鏈式存儲結構
線性表的順序存儲結構定義:
指的是用一段地址連續的存儲單元依次存儲線性表的數據元素
- 線性表的第一個數據元素的存儲位置稱為起始位置或基地址
- 順序表順序存儲結構必須占用一片連續的存儲空間,只要知道某個元素的存儲位置就可以知道其他元素的存儲位置
- 地址計算方法
- 假設線性表的每個元素需要c個存儲單元,則第i+1個數據元素的存儲位置和第i個數據元素的存儲位置之間的關系滿足:LOC(ai+1)=LOC(ai)+c
- 所以對於第i個數據元素ai的存儲位置可以由a1推算得出:LOC(ai)=LOC(a1)+(i-1)*c
【間隔i-1個存儲單元,間隔(i-1)*c個地址】
線性表順序存儲的結構代碼:
1 #define MAXSIZE 20 2 typedef int ElemType; 3 typedef struct 4 { 5 EleType data[MAXSIZE]; 6 int length; //線性表當前長度 7 } SqList;
總結下,順序存儲結構封裝需要三個屬性:
- 存儲空間的起始位置,數組data,它的存儲位置就是線性表存儲空間的存儲位置
- 線性表的最大存儲容量:數組的長度MaxSize
- 線性表的當前長度:length
線性表的基本操作
操作中用到的預定義常量和類型:
1 //函數結果狀態代碼 2 #define TRUE 1 3 #define FALSE 0 4 #define OK 1 5 #define ERROR 0 6 #define INFEASIBLE -1 7 #define OVERFLOW -2 8 9 //Status 是函數的類型,其值是函數結果狀態代碼,如OK等 10 //初始條件,順序線性表L已存在,1 <= i <=ListLength(L) 11 12 typedef int Status; 13 typedef char ElemType;
- 線性表L的初始化
1 Status InitList_Sq(SqList &L){ //構造一個空的順序表L 2 L.elem=new ElemType[MAXSIZE]; //為順序表分配空間 3 if(!L.elem)exit(OVERFLOW); //存儲分配失敗 4 L.length=0; //空表長度為0 5 return OK; 6 }
- 銷毀線性表
1 void DestroyList(SqList &L){ 2 if(L.elem) 3 { 4 free(L.elem);//釋放存儲空間 5 } 6 }
- 獲得元素操作
1 //操作結果:用e返回L中第i個數據元素的值 2 int GetElem(SqList L,int i,ElemType &e){ 3 if(i<1||i>L.length) //判斷i值是否合理,若不合理,返回ERROR 4 retrun ERROR; 5 else e = L.length[i-1]; //將i-1存儲單元的數據給e 6 return OK; 7 }
- 清空線性表
1 void ClearList(SqList &L){ 2 L.length=0; //將線性表的長度置為0 3 }
- 求線性表的長度
1 int GetLength(SqList L){ 2 return L.length; 3 }
- 判斷線性表L是否為空
1 int IsEmpty(SqList L){ 2 if(L.length==0)return 1; 3 else return 0; 4 }
- 順序表的查找(查找在L中與指定值e相同的數據元素的位置)
1 int LocateElem(SqList L,ElemType e){ 2 //在線性表L中查找值為e的數據元素,返回其序號(是第幾個元素) 3 for(i=0;i<L.length;i++) 4 { 5 if(L.elem[i]==e) 6 { 7 return i+1; //查找成功,返回序號 8 } 9 10 } 11 return 0; //查找失敗,返回0 12 }
- 插入操作,思路:
- 如果插入位置不合理,拋出異常;
- 如果線性表長度大於等於數組長度,則跑出異常或動態增加數組容量;
- 從最后一個元素開始向前遍歷到第i個位置,分別將它們都向后移動一個位置;
- 將要插入的元素插入位置i處;
- 線性表長度+1;
/*操作結果:在L中第i個位置之前插入新的數據元素e,L長度+1*/ Status ListInsert(Sqlist *L, int i,ElemType e) { int k; if(L->length == MAXSIZE) //順序線性表已經滿了 { return ERROR; } if (i<1 || i>L->length+1)//當i不在范圍內時 { return ERROR; } if(i <= L->length)//若插入數據位置不在表尾 { /*將要插入位置后數據元素向后移動一位*/ for (k=L->length-1; k>=i-1; k--) { L->data[k+1] = L->data[k]; } } L->data[i-1] = e;//將新元素插入 L->length++; return OK; }
- 刪除操作,思路:
- 如果刪除位置不合理,拋出異常;
- 取出刪除元素;
- 從刪除元素位置開始遍歷到最后一個元素位置,分別將它們都向前移動一個位置;
- 表長-1;
/* 操作結果:刪除L的第i個數據元素,並用e返回其值,L長度-1 */ Status ListDelete(Sqlist *L, int i,ElemType e) { int k; if(L->length == 0) //順序線性表已經滿了 { return ERROR; } if (i<1 || i>L->length)//當i不在范圍內時 { return ERROR; } *e = L->data[i-1];//賦值給e指針,返回其值 if(i <= L->length)//若刪除數據位置不在表尾 { /* 將刪除位置后面的元素向前移動一位 */ for ( k=i; k<L->length; k++) { L->data[k-1] = L->data[k]; } } L->length--;//表長-1 return OK; }
線性表順序存儲結構特性:
- 線性表的順序存儲結構,在存、讀數據時,時間復雜度都是O(1),而在插入、刪除時,時間復雜度都是O(n)
- 適合元素個數比較穩定,不經常插入和刪除元素,更多的操作時存取數據的應用
線性表順序存儲結構的優缺點:
- 優點:
- 無須為表示表中元素之間的邏輯關系而增加額外的存儲空間
- 可以快速地存取表中任意位置的元素
- 缺點:
- 插入和刪除操作需要移動大量元素
- 當線性表長度變化較大時,難以確定存儲空間的容量
- 容易造成存儲空間的“碎片”
線性表的鏈式存儲結構
線性表鏈式存儲結構定義
用一組任意的存儲單元存儲線性表的數據元素,鏈表中元素的物理位置和邏輯位置不一定相同。
- 結點:數據元素的存儲映像,有數據域和指針域兩部分組成
- 數據域:存儲數據元素信息的域
- 指針域:存儲指針(鏈)信息的域
- 鏈表:n個結點由指針鏈接成一個鏈表
- 單鏈表:每個結點只有一個指針域的鏈表(指針存后繼)
- 雙鏈表:每一個結點有兩個指針域的鏈表(前面的指針域存前驅,后面的指針域存后繼)
- 循環鏈表:(單雙)鏈表中最后一個結點的指針域指向頭結點
- 頭指針和頭結點:
- 頭指針:是指向鏈表中第一個結點的指針
- 頭指針是指鏈表指向第一個結點的指針,若鏈表有頭結點,則是指向頭結點的指針
- 頭指針具有標識作用,所以常用頭指針冠以鏈表的名字(指針變量的名字)
- 無論鏈表是否為空,頭指針均不為空
- 頭指針是鏈表的必要元素
- 頭結點:是在鏈表的首元結點之前附設的一個結點
- 頭結點是為了操作的統一和方便而設立的,放在第一個元素的結點之前,其數據域一般無意義(也可以用來存放鏈表的長度)
- 有了頭結點,對在第一元素結點前插入結點和刪除第一結點操作與其他結點的操作就統一了
- 頭結點不一定是鏈表的必須要素
- 頭指針:是指向鏈表中第一個結點的指針
單鏈表圖例:
空鏈表圖例:
單鏈表
- 用結構指針描述單鏈表
typedef struct Node{ //聲明結點類型和指向節點的指針類型 ElemType data; //結點的數據域 struct Node* Next; //結點的指針域 }Node; typedef struct Node* LinkList; //LinkList為指向結構體Node的指針類型
如果p->data = ai,那么p->next->data = ai+1
- 插入和刪除操作單鏈表時間復雜度都是O(1)
- 單鏈表的讀取,思路:
- 聲明一個結點p指向鏈表第一個結點,初始化j從1開始
- 當j<i時,就遍歷鏈表,讓p指針向后移動,不斷指向下一節點,j+1
- 若到鏈表末尾p為空,則說明第i個元素不存在
- 否則,查找成功,返回結點p的數據
/* 操作結果:用e返回L中第i個數據元素的值 */
//就是從頭開始找,直到第i個元素為止
Status GetElem(LinkList L,int i,ElemType *e) { int j; LinkList p;//聲明指針p p = L->next;//讓p指向L的第一個結點 j = 1; while (p && j<i)//p不能為空and還沒找到i { p = p->next; ++j; } if (!p || j>i)//遍歷完之后p為空還沒找到,或者j>i不符合條件 { return ERROR; } *e = p->data;//找到之后賦值給*e return OK; }
- 單鏈表的插入,思路:
- 聲明一結點p指向鏈表頭結點,初始化j從1開始
- 當j<i時,就遍歷鏈表,讓p的指針向后移動,不斷指向下一結點,j累計加1
- 若到鏈表末尾p為空,則說明第i個元素不存在
- 否則查找成功,在系統中生成一個空結點S
- 將數據元素e賦值給s->data
- 單鏈表的插入(s->next = p->next; p->next = s;)語句順序不能調換
- 返回成功
/* 操作結果:在L中第i個位置之前插入新的數據元素e,L的長度加1 */ Status ListInsert(LinkList *L,int i,ElemType e) { int j; LinkList p,s;//聲明指針p,s p = *L j = 1; while (p && j<i)//尋找第i個結點 { p = p->next; ++j; } if (!p || j>i)//遍歷完之后p為空還沒找到,或者j>i不符合條件 { return ERROR; } s = (LinkList)malloc(sizeof(Node)) //獲取Node的字段長度,然后強轉為Linklist類型。 //S變量就代表地址長度和Node一樣所占內存空間同樣大小的Linklist s->data = e; s->next = p->next;//插入操作不能調換順序 p->next = s; return OK; }
- 單鏈表的刪除,思路:
- 聲明一結點p指向鏈表頭結點,初始化j從1開始
- 當j<i時,就遍歷鏈表,讓p的指針向后移動,不斷指向下一結點,j累計加1
- 若到鏈表末尾p為空,則說明第i個元素不存在
- 否則查找成功,將欲刪除結點p->next = q->next
- 將q結點中的數據賦值給e,作為返回
- 釋放q結點
/* 操作結果:刪除L的第i個數據元素,並用e返回其值,L的長度-1 */ Status ListDelete(LinkList *L,int i,ElemType *e) { int j; LinkList p,q;//聲明指針p,q p = *L j = 1; while (p->next && j<i)//尋找第i個結點 { p = p->next; ++j; } if (!(p->next) || j>i)//遍歷完之后p為空還沒找到,或者j>i不符合條件 { return ERROR; } q = p->next;//刪除操作 || p->next = p->next->next; p->next = q->next; *e = q->data;//返回刪除值 free(q);//釋放q return OK; }
- 單鏈表的整表創建,思路:
-
聲明一結點p和計數器變量i
-
初始化一空鏈表L
-
讓L的頭結點的指針指向NULL,即建立一個帶頭結點的單鏈表
-
循環實現后繼結點的賦值和插入
-
- 頭插法建立單鏈表,思路:
- 先讓新節點的next指向頭節點之后
- 然后讓表頭的next指向新節點
/* 頭插法建立單鏈表示例 */ void CreateListHead(LinkList *L,int n) { LinkList p; int i; srand(time(0));//初始化隨機數種子 //srand函數是隨機數發生器的初始化函數 *L = (LinkList)malloc(sizeof(Node)); (*L)->next = NULL; for ( i = 0; i < n; i++) { p = (LinkList)malloc(sizeof(Node));//生成新結點 p->data = rand()%100+1; /* rand() 是產生一個容隨機整數的函數,其分布范圍是0到最大的整數, rand() %100 指和100取余,得到一個0到99整數 rand() %100 +1 得到一個1到100的整數 */ p->next = (*L)->next;//p指向下一個 (*L)->next = p;//再將p給單鏈表L的表頭 } }
- 尾插法建立單鏈表,思路:
- 把新結點都插入到最后,這種算法稱之為尾插法。
(小甲魚給這個算法想到一個容易記住的藝名,叫“菊花”)/* 尾插法建立單鏈表演示 */ void CreateListTail(LinkList *L, int n) { LinkList p, r; int i; srand(time(0)); *L = (LinkList)malloc(sizeof(Node));//開辟空間給*L r = *L;//把頭結點賦給r for( i=0; i < n; i++ ) { p = (Node *)malloc(sizeof(Node));//隨機分配空間給p p->data = rand()%100+1;//給p隨機賦值 r->next = p;//讓r指向p r = p;//讓p賦給r //循環操作來看 } r->next = NULL;//建表結束,讓r->next指向空NULL }
- 把新結點都插入到最后,這種算法稱之為尾插法。
- 單鏈表的整表刪除,思路:
- 聲明結點p和q
- 將第一個結點賦值給p,下一結點賦值給q
- 循環執行釋放p和將q賦值給p的操作
Status ClearList(LinkList *L) { LinkList p, q; p = (*L)->next; while(p) { q = p->next; free(p); p = q; } (*L)->next = NULL; return OK; }
單鏈表結構與順序存儲結構優缺點
分別從存儲分配方式、時間性能、空間性能三方面來做對比
-
存儲分配方式:
- 順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素
- 單鏈表采用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素
-
時間性能:
- 查找
- 順序存儲結構O(1)
- 單鏈表O(n)
- 插入和刪除
- 順序存儲結構需要平均移動表長一半的元素,時間為O(n)
- 單鏈表在計算出莫位置的指針后,插入和刪除時間僅為O(1)
- 查找
-
空間性能:
- 順序存儲結構需要預分配存儲空間,分大了,容易造成空間浪費,分小了,容易發生溢出
- 單鏈表不需要分配存儲空間,只要有就可以分配,元素個數也不受限制
-
綜上所述:
- 若線性表需要頻繁查找,宜用順序存儲結構
- 若需要平凡插入和刪除,宜用單鏈表結構
- 比如說游戲開發中,對於用戶注冊的個人信息,除了注冊時插入數據外,絕大多數情況都是讀取,所以應該考慮用順序存儲結構
- 而游戲中的玩家的武器或者裝備列表,隨着玩家的游戲過程中,可能會隨時增加或刪除,單鏈表結構就可以大展拳腳了
- 當線性表中的元素個數變化較大或者根本不知道有多大時,最好用單鏈表結構,這樣可以不需要考慮存儲空間的大小問題
- 而如果事先知道線性表的大致長度,比如一年12個月,一周就是星期一至星期日共七天,這種用順序存儲結構效率會高很多
線性表的順序存儲結構和單鏈表結構各有其優缺點,不能簡單的說哪個好,哪個不好,需要根據實際情況,來綜合平衡采用哪種數據結構更能滿足和達到需求和性能