數據結構(3)線性表之靜態鏈表

前言

靜態鏈表,就是用數組描述的鏈表。靜態鏈表在初始化時要確定數組的長度,所以需要預先分配空間,其空間大小一般是靜態的,故名靜態鏈表(這一點同之前所說的順序表相似)。先前所說的單鏈表,是在創建和插入時調用malloc函數來申請空間,其空間大小是動態分配的,所以屬於動態鏈表。

用數組表示線性表

數組下標在某種意義上可以看作是地址,在一些沒有指針的語言中(例如Basic、Fortran),就能利用數組下標達到訪問某個內存空間的效果,繼而可以實現鏈式表示。(實際上,利用數組下標同樣可以實現順序表示)

順序表示

用數組來實現順序表示非常簡單,只需要把數據存入數組中即可。我們之前所寫的順序表類似於自己實現了數組的相關操作。

img_1

鏈式表示

用數組來實現鏈式表示,則數組中需要存儲的是一個結構體,與鏈表相似,結構體由兩個部分組成:一個保存數據的數據域和一個指向後繼結點的指針域。只不過在數組中,這個指針域並不存放後繼結點的地址,而是存放後繼結點所在位置的下標。

img_2

靜態鏈表的具體設計

  • 採用有頭結點的結構,則數組第一位爲頭結點,數據域不儲存數據,cur存放首元結點的下標

  • 鏈表進行插入時,仍需申請空間,但是不再從整個內存中申請空間,而是從數組中申請空間(稱未被使用的數組部分爲備用鏈表),因此還需要一個結點來存儲備用鏈表第一個結點的下標。

    img_3

靜態鏈表的初始化

先前說,在插入鏈表時需要從pool中去申請空間,就是通過pool結點的cur值得到備用鏈表第一個結點的下標。那麼pool結點如何知道備用鏈表從哪裏開始呢?根據我們設計的結構,可以知道我們是從下標爲2的地方開始存儲數據,第一個方法是利用一個變量index來記錄,初始值爲2,每申請一個空間index便加1,當index值大於數組的大小時,說明已經沒有多餘空間了,申請失敗。

第二個方法是,除頭結點外,設置每個結點的cur值爲它後一個位置的下標(pool也不例外,這樣相當於初始化pool的cur爲2)。這樣在申請結點空間時,將pool的cur所指的結點拿來存放數據,再把該結點的cur值交給pool。相當於從備用鏈表中取出一個結點,再將下一個結點拿來備用。這樣做要特別標識數組最後一位的cur值,以免越界。

img_4
這裏我採用的是第二種方法,因此初始化鏈表主要包括三個部分的內容

  • 設置頭結點的cur值

  • 初始化除頭結點外的結點的cur值

  • 設置數組最後一位的cur值

    void InitSList(StaticList space){
        //初始化除頭結點外的結點的cur值
        for (int i = 1; i < MAX_SIZE; i ++) {
            space[i].cur = i+1;
        }
        //設置數組最後一位的cur值
        space[MAX_SIZE-1].cur = 0;
        //設置頭結點的cur值
        space[0].cur = -1;
    }
    

靜態鏈表的插入

靜態鏈表的插入操作主要分爲兩個部分

  • 從備用鏈表處申請結點
  • 將數據存入申請來的結點中、設置有關cur值

但是,插入有多種方式:頭部插入、尾部插入、按位置插入、按值插入……要實現不同的插入方式,和cur值的設置有關,和申請結點無關;如果要實現多個插入方式,這個操作無疑是重複了。所以爲了效率起見,我們可以把它單獨作爲一個方法,這樣在實現不同插入時就只需要調用函數獲得結點即可。

Malloc_SL方法從備用鏈表中申請空間

int Malloc_SL(StaticList space){
    
    //獲得備用鏈表第一個結點的下標
    int i = space[1].cur;
    
    if (space[1].cur != 0) {
        //還有備用空間,取得備用空間
        space[1].cur = space[i].cur;
    }
    //將下標返回
    return i;
}

頭部插入

通過head處的cur值找到鏈表第一個結點,設置新生成結點的cur值爲第一個結點的下標,再設置head處的cur值爲新結點的下標

注:用-1表示到達鏈表尾部

img_5

尾部插入

除第一次插入外,head處的cur值不用變,且新生成結點的cur值都爲-1,只需要找到原有鏈表的最後一個結點,再修改該結點的cur值爲新生成的結點即可

img_6

按位置插入

回顧一下我們在數組中按位置插入的方法:找到要插入的位置,將從該位置起所有元素往後移一位(假設沒有越界),再將數據保存到要插入的位置上。

按理來說,靜態鏈表是用數組實現的,應該也可以使用這個方式。但是,我們設計的結構裏用遊標(cur)來控制鏈表的邏輯順序,數據實際在數組中的存放順序可能是不同的。因此要挪動數據,不能像數組中一樣簡單地倒序遍歷、存值,而是需要像鏈表一樣一個個去找位置。由於我們使用的還是單向鏈表,這樣的遍歷就更爲困難了。

好消息是,也正由於遊標的存在,可以通過修改cur值來修改鏈表的邏輯結構,不需要移動大量的數據。這也是鏈表的普遍優點:在插入、刪除時十分方便

img_7

靜態鏈表的刪除

靜態鏈表的刪除也分爲兩個部分

  • 修改cur的指向
  • 將被刪除的結點回收到pool中

與插入類似,回收結點的操作也可以單獨用一個函數來完成。這裏只寫了頭部刪除的操作。
img_9

在編程實現時,不同資料裏對函數參數的書寫有所不同

void InitSList0(StaticList space);
void InitSList1(ListNode *space);

void InitSList2(StaticList &space);

常見的是 InitSList0 和 InitSlist2 這樣的寫法。我們對鏈表本身進行修改,所需要的是鏈表的地址,InitSList0 是將數組作爲函數參數傳遞,實際上就是把數組的首地址(第一個元素的地址)傳了過來,它本身是一個ListNode類型的地址,因此使用 InitSList1 這樣的寫法也同樣可行。以上兩種寫法中,space都只是用於接收的形參名。

而 InitSlist2 這樣的寫法,傳遞的是數組的引用,相當於給數組起了一個別名space,無論是操作別名space還是操作數組本身的名字,實際上操作的是同一片空間的內容,所以達到的效果是一致的。

兩者的區別可能在傳遞數組時不太明顯,那麼可以看下面的例子

img_8
要注意的一點是,C語言中並沒有“引用”這一概念,這是C++纔有的概念,如果按 InitSlist2 的方式書寫並且能運行說明使用的是C++的編譯器。

全部代碼

StaticList.h

#ifndef StaticList_h
#define StaticList_h

#include <stdio.h>

#define MAX_SIZE 20
#define ElemType char

typedef struct ListNode{
    ElemType data;
    int cur;
}ListNode;

typedef ListNode StaticList[MAX_SIZE];

//從pool中分配空間
int Malloc_SL(StaticList space);
//從pool中釋放空間
void Free_SL(StaticList space,int i);

//初始化靜態鏈表
void InitSList(StaticList space);
void InitSList1(ListNode *space);
void InitSList2(StaticList &space);


//頭部插入
void Insert_fount(StaticList space,ElemType x);
//尾部插入
void Insert_back(StaticList space,ElemType x);
//按位置插入
void Insert_pos(StaticList space,ElemType x,int pos);

//展示
void ShowSList(StaticList space);
//刪除
void Delete(StaticList space);

#endif /* StaticList_h */

StaticList.cpp

#include "StaticList.h"

//從pool中分配空間
int Malloc_SL(StaticList space){
    
    //獲得備用鏈表第一個結點的下標
    int i = space[1].cur;
    
    if (space[1].cur != 0) {
        //還有備用空間,取得備用空間
        space[1].cur = space[i].cur;
    }
    //將下標返回
    return i;
}

//從pool中釋放空間
void Free_SL(StaticList space,int i){
    space[i].cur = space[1].cur;
    space[1].cur = i;
}

//初始化靜態鏈表
void InitSList(StaticList space){
    //初始化除頭結點外的結點的cur值
    for (int i = 1; i < MAX_SIZE; i ++) {
        space[i].cur = i+1;
    }
    //設置數組最後一位的cur值
    space[MAX_SIZE-1].cur = 0;
    //設置頭結點的cur值
    space[0].cur = -1;
}
void InitSList1(ListNode *space){
    //初始化除頭結點外的結點的cur值
    for (int i = 1; i < MAX_SIZE; i ++) {
        space[i].cur = i+1;
    }
    //設置數組最後一位的cur值
    space[MAX_SIZE-1].cur = 0;
    //設置頭結點的cur值
    space[0].cur = -1;
}

void InitSList2(StaticList &space){
	for(int i = 1;i < MAX_SIZE; i ++){
		space[i].cur = i+1;
	}
	//設置數組最後一位的cur值
    space[MAX_SIZE-1].cur = 0;
    //設置頭結點的cur值
    space[0].cur = -1;
}


//頭部插入
void Insert_fount(StaticList space,ElemType x){
    //從備用鏈表中申請空間
    int i = Malloc_SL(space);
    
    if (i == 0) {
        //鏈表沒有空間了
        printf("申請結點空間失敗");
        return;
    }
    
    space[i].data = x;
    
    //判斷是否是第一個結點
    if (space[0].cur == -1) {
        //是第一個,把該結點的指向改爲-1
        space[i].cur = -1;
        //修改head的指向
        space[0].cur = i;
    }else{
        space[i].cur = space[0].cur;
        //修改head的指向
        space[0].cur = i;
    }
}

//按位置插入
void Insert_pos(StaticList space,ElemType x,int pos){
	
	//1.1從備用鏈表中申請空間
    int i = Malloc_SL(space);
	
	if (i == 0) {
        //鏈表沒有空間了
        printf("申請結點空間失敗!\n");
        return;
    }


    //判斷要插入的位置是否合法
    if (pos > i-1) {
        printf("要插入的位置不合法!\n");
        return;
    }else if(pos == 1){
		//直接進行頭部插入
		//如果不直接進行頭部插入,也要額外判斷,因爲要修改head的cur值
		Insert_fount(space,x);
	}else{
		//1.2存值
		space[i].data = x;
	

		//2.1遍歷鏈表,找到插入位置
		int k = space[0].cur;
		while(k != pos){
			printf("pos = %d\n",pos);
			printf("k = %d\n",k);
			k = space[k].cur;
		}

		//2.2插入
		space[i].cur = space[k].cur;
		space[k].cur = i;
    
	}

}

//尾部插入
void Insert_back(StaticList space,ElemType x){
    //從備用鏈表中申請空間
    int i = Malloc_SL(space);
    
    if (i == 0) {
        //鏈表沒有空間了
        printf("申請結點空間失敗");
        return;
    }
    
    space[i].data = x;
    space[i].cur = -1;
    
    //尋找到原有鏈表的最後一個結點
    int k = space[0].cur;
    while (space[k].cur != -1) {
        k = space[k].cur;
    }
    //判斷是否是第一個結點
    if (k == -1) {
        //是,修改head的指向
        space[0].cur = i;
    }else{
        //將結點連接到最末尾
        space[k].cur = i;
    }
}

//展示
void ShowSList(StaticList space){
    //獲得第一個結點的位置
    int i = space[0].cur;
    
    //遍歷鏈表
    while (i != -1) {
        printf("  %c",space[i].data);
        
        //更改i的指向
        i = space[i].cur;
    }
    printf("\n");
}

//頭部刪除
void Delete(StaticList space){
    int i = space[0].cur;
    
    space[0].cur = space[i].cur;
    
    //將該空間回收
//    space[i].cur = space[1].cur;
//    space[1].cur = i;
    Free_SL(space, i);
}

Main.cpp

#include "StaticList.h"

int main(int argc, const char * argv[]) {
    StaticList SL;

    InitSList(SL);
	//InitSList1(SL);
	//InitSList2(SL);
    
    for (int i = 0; i < 5; i ++) {
        Insert_back(SL, 'A'+i);
		//Insert_fount(SL,'a'+i);
    }
    ShowSList(SL);

	Insert_pos(SL,'Z',2);
	ShowSList(SL);
    
    Delete(SL);
    ShowSList(SL);
    
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章