1. 程式人生 > >資料結構理論基礎-2—線性表

資料結構理論基礎-2—線性表

Table of Contents

線性表

插入操作

刪除操作

靜態連結串列

迴圈連結串列

雙向連結串列

線性表

線性表(List):零個或多個數據元素的有限序列

用數學語言進行定義,如下:

若將線性表記為(a_{1},...,a_{i-1}, a_{i}, a_{i+1},..., a_{n}),稱 a_{i-1} 是 a_{i} 的直接前驅元素,a_{i+1} 是 a_{i} 的直接後繼元素。當i = 1, 2,...,n-1時, a_{i} 有且僅有一個直接後繼,當 i = 2, 3, ...,n 時, a_{i} 有且僅有一個直接前驅。

線性表的抽象資料型別如下:

ADT 線性表(List)

Data

(數學語言定義)

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):線上性表的第 i 個位置插入元素 e

ListDelete(*L, i, *e):刪除線性表 L 中第 i 個位置元素,並用 e 返回其值

ListLength(L):返回線性表 L 的元素個數

線性表的順序儲存結構

線性表的順序儲存結構:指的是用一段地址連續的儲存單元依次儲存線性表的資料元素(可以用C語言中的一維陣列實現)

# define MAXSIAE 20        /*儲存空間初始分配量*/
typedef int ElemType;      /*ElemType型別根據實際情況而定,這裡假設為int*/
type struct{
    Elemtype data[MAXSIZE];/*陣列儲存資料元素,最大值為MAXSIZE*/
    int length;            /*線性表當前長度*/
}SqList;

獲得元素操作

(實現 GetElem(L, i, *e) 操作,時間複雜度為O(1) )

# define OK  1
# define ERROR 0
# define TURE 1
# define FALSE 0
typedef int Status;

/*操作結果:用e返回L中第i個數據元素的值,i從1開始,與陣列序號相差一個,下同*/
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;
}

插入操作

(實現 ListInsert(*L, i, e) 操作,時間複雜度為O(n) )

插入操作思路:

  • 如果插入位置不合理,丟擲異常
  • 若線性表長度大於陣列長度,丟擲異常或動態增加容量
  • 從最後一個元素向前遍歷到第 i 個位置,分別將它們向後移動一個位置
  • 將要插入的元素 e 填入到位置 i 處
  • 表長加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;
}

刪除操作

(實現ListDelete(*L, i, *e) 操作,時間複雜度為O(n) )

刪除操作的思路:

  • 如果刪除位置不合理,丟擲異常
  • 取出刪除元素
  • 從刪除元素位置開始遍歷到最後一個元素位置,分別將它們向前移動一個位置
  • 表長減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)
		return ERROR;
	*e = L->data[i-1];
	if(i < L->length)
	{
		for(k=i; k<L->length; k++)
			L->data[k-1] = L->data[k];
	}
	L->length--;
	return OK;
}

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

優點:

  • 無需為表示表中元素之間的邏輯關係而增加額外的儲存空間
  • 可以快速存取表中任一位置的元素

缺點:

  • 插入和刪除操作需要移動大量元素
  • 當線性表長度變化較大時,難以確定儲存空間的容量
  • 造成儲存空間的“碎片”

線性表的鏈式儲存結構

順序儲存結構的最大缺點是:插入和刪除時需要移動大量元素,原因就在於:順序儲存結構中的相鄰元素,它們的儲存位置也是相鄰的,解決辦法就是:讓儲存位置不再具有任何關係性。

線性表的鏈式儲存結構的特點就是:線性表中的資料元素可在儲存在記憶體未被佔用的任意位置,這些儲存單元可以是連續的,也可以是不連續的。

為了表述資料元素 a_{i} 和其直接後繼元素a_{i+1}之間的邏輯關係,對a_{i}來說,除了儲存其本身的資訊外,還要儲存直接後繼a_{i+1}的儲存位置。我們把儲存資料元素資訊的域稱為資料域,把儲存直接後繼位置的域稱為指標域,指標域中儲存的資訊稱為指標或鏈,資料域和指標域組成元素a_{i}的儲存映像,稱為結點(Node)

n個結點鏈結成一個連結串列,即為線性表的鏈式儲存結構,因為此連結串列的每個結點只包含一個指標域,所以叫單鏈表

單鏈表正是通過每個結點的指標域將線性表的資料元素按其邏輯次序連結在一起

                                                  

      

  

                         

/*線性表的單鏈表儲存結構*/
typedef struct Node
{
	ElemType data;
	struct Node *next;
}Node;
typedef struct Node *LinkList

從這個結構定義中可知,結點由存放資料元素的資料域和存放後繼結點地址的指標域組成

假設 p 是指向線性表第 i 個元素的指標,則:

                                          

單鏈表的讀取操作

(實現 GetElem(L, i, *e) 操作,時間複雜度為O(n) )

讀取操作思路:

  1. 宣告一個結點 p 指向連結串列第一個結點,初始化 j 從1開始
  2. 當 j < i 時,就遍歷連結串列,讓 p 的指標向後移動,j++
  3. 若到連結串列末尾 p 為空,則說明第 i 個元素不存在
  4. 否則查詢成功,返回結點 p 的資料
/*操作結果:用e返回L中第i個數據元素的值*/
Status GetElem(LinkList L, int i, ElemType *e)
{
	int j;
	LinkList p; 	/*宣告一個結點p*/
	p = L->next;	/*讓p指向連結串列L的第一個結點*/
	j = 1;			/*j為計數器*/
	while(p && j<i) /*p不為空或計數器j還沒有等於i時,迴圈繼續*/
	{
		p = p->next; 
		j++;
	}
	if(!p || j>i)
		return ERROR;
	*e = p->data;
	return OK;
}

單鏈表的插入

       

由圖可知,根本不用移動其他結點,只需要:

s -> next = p -> next;    p -> next = s; (注意,兩句不能顛倒!!

既是:讓 p 的原後繼結點改成為 s 的後繼結點,再把 s 結點變成 p 的新後繼結點

插入操作(實現 ListInsert(*L, i, e) 操作,時間複雜度為O(n) )

插入操作思路:

  1. 宣告一個結點 p 指向連結串列第一個結點,初始化 j 從1開始
  2. 當 j < i 時,就遍歷連結串列,讓 p 的指標向後移動,j++
  3. 若到連結串列末尾 p 為空,則說明第 i 個元素不存在
  4. 否則查詢成功,在系統中生成一個空結點s
  5. 將資料元素 e 賦值給 s->data
  6. 單鏈表的插入標準語句 s -> next = p -> next;  p -> next = s;
  7. 返回成功
/*操作結果:在 L 中第 i 個位置之前插入新的元素 e, L 的長度加 1*/
Status ListInsert(LinkList L, int i, ElemType e)
{
	int j;
	LinkList p, s; 	
	p = *L;	
	j = 1;			
	while(p && j<i) /* 尋找第 i 個結點*/
	{
		p = p->next; 
		j++;
	}
	if(!p || j>i)
		return ERROR;  /* 第 i 個元素不存在 */
	s = (LinkList)malloc(sizeof(Node))  /* C語言中的 malloc 函式作用是生成一個新的結點,其型別                                                
                              和 Node 一致,實質就是在記憶體中找一塊空地,用來存放 e 資料 s 結點*/
        s->data = e;
        s->next = p->next;
        p->next = s;
	return OK;
}

單鏈表的刪除

                                          

實際上就是一步,p->next = p->next->next (也就是讓 p 的後繼的後繼結點改成 p 的後繼結點):

如果用 q 來取代 p->next,則

q = p->next;  p->next = q->next;

刪除操作(實現 ListDelete(*L, i, *e) 操作,時間複雜度為O(n) )

刪除操作思路:

  1. 宣告一個結點 p 指向連結串列第一個結點,初始化 j 從1開始
  2. 當 j < i 時,就遍歷連結串列,讓 p 的指標向後移動,j++
  3. 若到連結串列末尾 p 為空,則說明第 i 個元素不存在
  4. 否則查詢成功,將欲刪除的結點 p-next 賦值給 q
  5. 單鏈表的刪除標準語句   p -> next = q -> next;
  6. 將 q 結點中的資料賦值給 e 
  7. 釋放 q 結點
  8. 返回成功
/*操作結果:刪除 L 中第 i 資料元素,並用 e 返回其值, L 的長度減 1*/
Status ListDelete(LinkList L, int i, ElemType *e)
{
	int j;
	LinkList p, q; 	
	p = *L;	
	j = 1;			
	while(p->next && j<i) /* 尋找第 i 個元素*/
	{
		p = p->next; 
		j++;
	}
	if(!(p->next) || j>i)
		return ERROR;  /* 第 i 個元素不存在 */
	q = p->next;
        p->next = q->next;
        free(q);
	return OK;
}

分析可發現,單鏈表的插入和刪除操作看起來並不比順序儲存結構簡單,複雜度也都是 O(n)。其實不然,當我們想從第 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 個元素的值,建立帶表頭結點的單鏈表線性表L(尾插法)*/
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

        將 q 賦值給 p

/* 將 L 重置為空表*/
void ClearList(LinkList *L)
{
    LinkList p, q;
    p = (*L)->next;
    while(p)
    {
        q = p->next;
        free(p);
        p = q;
    }
    (*L)->next = NULL;  /*頭節點指標域為空*/
    return OK;
}

靜態連結串列

靜態連結串列是指當某種語言沒有指標能力的時候,人們想用陣列來代替指標,來描述連結串列。讓陣列的元素由兩個資料域 data 和 cur 組成,資料域 data 用來存放資料元素,遊標 cur 相當於 next 指標,存放該元素的後繼在陣列中的下標。

/* 線性表的靜態連結串列儲存結構 */
#define MAXSIZE 1000  /* 假設連結串列的最大長度是1000 */
typedef struct
{
    ElemType data;
    int cur;  /* 遊標(cursor)為 0 時表示無指向 */
}Component, StaticLinkList[MAXSIZE];

        

如上圖所示,我們對陣列的第一個和最後一個元素作為特殊元素處理,不存放資料。陣列第一個元素的 cur 存放備用連結串列(未被使用的資料元素)的第一個結點的下標,陣列最後一個元素的 cur 存放第一個有數值的元素的下標。程式碼如下:

Status InitList(StaticLinkList space)
{
    int i;
    for(i=0; i<MAXSIZE-1; i++)
        space[i].cur = i+1;
    space[MAXSIZE-1].cur = 0;
    return OK;
}

假設把 甲 乙 丁等資料存入靜態連結串列的話,則如下圖所示:

                             

靜態連結串列的插入操作

靜態連結串列中要解決的是:如何用靜態模擬動態連結串列結構的儲存空間的分配,需要時申請(malloc()函式),無用時釋放(free()函式)

為了辨明陣列中哪些分量未被使用,解決的辦法是將所有未被使用過的及已被刪除的分量用遊標鏈成一個備用的連結串列,每當進行插入時,便可以從備用連結串列上取得第一個結點作為待插入的新結點。

int Malloc_SLL(StaticLinkList space)
{
    int i = space[0].cur;  /* i 就是第一個備用空閒的下標 */
    if(space[0].cur)
        space[0].cur = space[i].cur;  /* 由於拿出一個分量來使用了,所以就得把它的下一個分量用來 
                                         做備用 */
    return i;
}

這段程式碼,拿上圖 甲 乙 丁的例子來說,下標為7的分量準備要使用了,就得有接替者,所以就把分量 7 的 cur 值賦值給頭元素,也就是把 8 給 space[0].cur,實現類似malloc()函式的使用。

那麼如何把 丙 存放在 乙 和 丁 中間?

              

如圖所示,只需要把 乙 放在空閒分量7的位置,把原來的 乙.cur = 3  改為  丙.cur = 3, 乙.cur = 7.

/* 在 L 中第 i 個元素之前插入新的資料元素 e */
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
    int j, k, l;
    k = MAXSIZE - 1;
    if(i<1 || i>ListLength(L) + 1)
        return ERROR;
    j = Malloc_SSL(L);  /* 獲得空閒分量的下標 */
    if(j)
    {
        L[j].data = e;  /* 將資料賦值給此分量的data */
        for(l=1; l<i-1; l++)  /* 找到第 i 個元素之前的位置 */
            k = L[k].cur;
        L[j].cur = L[k].cur;  /* 把第 i 個元素之前的 cur 賦值給新元素的cur,即 丙.cur=3 */
        l[k].cur = j;  /* 把新元素的下標賦值給第 i 個元素之前元素的cur,即 乙.cur=7 */
        return OK;
    }
}

靜態連結串列的刪除操作

如何刪除連結串列中的 “甲”?

             

意思就是,刪除了 甲 之後,位置空了出來,如果之後有插入操作的話,優先考慮這裡

/* 刪除在L中第 i 個數據元素 e */
Status ListDelete(StaticLinkList L, int i)
{
    int j, k;
    if(i<1 || i>ListLength(L))
        return ERROR;
    k = MAXSIZE - 1;
    for(j=1; j<i-1; j++)
        k = L[k].cur;
    j = L[k].cur;
    L[k].cur = L[j].cur;
    Free_SSL(L, j);
    return OK;
}

/* 將下標為 k 的空閒結點回收到備用連結串列 */
void Free_SSL(StaticLinkList space, int k)
{
    space[k].cur = space[0].cur;  /* 把第一個元素 cur 值賦給要刪除的分量cur */
    space[0].cur = k;  /* 把要刪除的分量下標賦值給第一個元素的cur */
}

Free_SLL()函式中的兩句,也就是 space[1].cur = space[0].cur = 8;   space[0].cur = k = 1;看刪除前後兩張圖的變化就能理解

靜態列表用的較少,優缺點如下:

優點:

    在插入和刪除操作時,只需要修改遊標,不需要移動元素。

缺點:

    沒有解決連續儲存分配帶來的表長難以確定的問題

    失去了順序儲存結構隨機存取的特性

迴圈連結串列

迴圈連結串列:將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列

迴圈列表解決了一個很麻煩的問題:如何從當中一個結點出發,訪問到連結串列的全部結點

           

雙向連結串列

雙向連結串列:是在單鏈表的每個結點中,再設定一個指向其前驅結點的指標域。

/* 線性表的雙向連結串列儲存結構 */
typedef struct DulNode
{
    ElemType data;
    struct DulNode *prior;  /* 直接前驅指標 */
    struct DulNode *next;  /* 直接後繼指標 */
}DulNode *DuLinkList;

  

雙向連結串列的插入和刪除操作並不複雜,只是順序比較重要。

     

/* 插入順序 */
s -> prior = p;            /* 把 p 賦值給 s 的前驅 */
s -> next = p -> next;     /* 把 p->next 賦值給 s 的後繼 */   
p ->next -> prior = s;    /* 把 s 賦值給 p->next 的前驅 */
p -> next = s;            /* 把 s 賦值給 p 的後繼 */

/* 刪除順序 */
p -> prior ->next = p -> next;    /* 把 p->next 賦值給 p->prior的後繼 */
p -> next -> prior = p -> prior;  /* 把 p->prior 賦值給 p->next的前驅 */   
free(p);