資料結構和演算法(一)線性表實現

孔雨露發表於2020-04-06

@TOC

1. 資料結構簡介

資料結構簡介

1.1 抽象資料型別

1.1.1 資料型別

資料型別:是指一組性質相同值的啟用以及定義在此啟用的一些操作的總稱。

在C語言中,按照取值的不同,資料型別可以分為2類:

  • 原子型別: 是不可以在分解的基本資料型別,包含整型,浮點型,字元型等。
  • 結構型別: 由若干型別組合而成,是可以再分解的,例如,整型陣列就是由若干整型資料組成的。

1.1.2 抽象資料型別

抽象:是抽取出事物具有的普遍的本質。它是抽出問題的特徵二忽略非本質的細節,是對具體事物的一個概括。抽象是一種思考問題的方式,它隱藏繁雜的細節,只保留實現目標必需要的資訊。

抽象資料型別:是指一個數學模型以及定義在該模型上的一組操作;例如,我們在編寫計算機繪圖軟體系統是,經常會使用到座標。也就是說,會經常使用x,y來描述縱橫座標。而在3D系統中,Z深度就會出現。既然這3個整型數字是始終出現在一起,那麼就可以定義成一個Point的抽象資料型別。它有x,y,z三個整型變數。這樣開發者就非常方便操作Point資料變數。

抽象資料型別可以理解成實際開發裡經常使用的結構體和類;根據業務需求定義合適的資料型別和動作。

在我們生活中有哪些線性表的例子呢?

例如: 26個字母表;例如學生基本資訊表。每個學生為一個資料元素,包含學號,姓名,專業等資料專案。滿足資料元素不同,但是在同一個線性表中的元素必定具有相同的特點,即屬於同一資料物件,相鄰資料元素之間存在這個序列關係,注入此類有(n >= 0 )個資料特性相同的元素構成的有限序列稱為:“線性表”

/*
 資料: 程式的操作物件,用於描述客觀事物.
 資料的特點: 1️⃣ 可以輸入到計算機 2️⃣ 可以被計算機處理
 
 資料項: 一個資料元素由若干資料項組成
 資料元素: 組成資料的物件的基本單位
 資料物件: 性質相同的資料元素的集合(類似於陣列)
 
 結構: 資料元素之間不是獨立的,存在特定的關係.這些關係即是結構;
 資料結構:指的資料物件中的資料元素之間的關係
 */
#include <stdio.h>

//宣告一個結構體型別
struct Teacher{     //一種資料結構
    char *name;     //資料項--名字
    char *title;    //資料項--職稱
    int  age;       //資料項--年齡
};


int main(int argc, const char * argv[]) {
   
    struct Teacher t1;     //資料元素;
    struct Teacher tArray[10]; //資料物件;
    
    t1.age = 18;       //資料項
    t1.name = "CC";    //資料項
    t1.title = "講師";  //資料項
    
    printf("老師姓名:%s\n",t1.name);
    printf("老師年齡:%d\n",t1.age);
    printf("老師職稱:%s\n",t1.title);
    
    return 0;
}
複製程式碼

線性表中的元素的個數n定義為線性表的長度,如果n=0則稱為空表

對應非空的線性表和線性結構,其特點如下:

  • 存在唯一的一個被稱作“第一個”的資料元素
  • 存在唯一一個被稱作“最後一個”的資料元素。
  • 除了第一個之外,結構中的每個資料元素都有一個前驅。
  • 除了最後一個之外,結構中的每個資料元素都有一個後繼。

1.2 資料結構基本術語

1.2.1 資料結構基本術語

資料的構成:基本資料單位

資料的構成

  • 資料結構的邏輯結構:按邏輯分為:集合結構,線性結構,樹形結構,圖形機構等。

集合結構: 集合結構中的資料元素除了同屬於一個集合外,它們之間沒有其他關係,它們之間唯一的相同點就是"同屬於一個集合"。

集合結構

線性結構: 線性結構中的資料元素之間的關係是一對一的。常用的線性結構有:線性表、棧、佇列、雙佇列、陣列、串。

線性結構

樹形結構: 樹形結構中的資料元素是一對多的層級關係。常見的樹形結構: 二叉樹、B樹、哈夫曼樹、紅黑樹等。

樹形結構

圖形結構: 圖形結構中的資料元素之間的關係是多對多的。常見的圖形結構:鄰近矩陣、鄰接表。

圖形結構

  • 資料結構按物體機構分為:順序儲存結構,鏈式儲存結構。

順序儲存結構:指把資料元素存放在地址連續的儲存單元裡,其資料間的邏輯關係和物理關係是一致的。比如一個陣列,它的元素是一個接一個,在記憶體空間中的地址也是連續的,我們可以通過陣列的下標訪問每一個元素,也可以使用地址遞增的方式訪問

順序儲存結構

//順序表結構
typedef struct KSequenceList {
    KElementType *data;
    int length;
}KSList;
複製程式碼

鏈式儲存結構: 是把資料元素放在任意的儲存單元裡,這組儲存單元可以是連續的,也可以是不連續的。資料元素的儲存關係並不能反映邏輯關係,因此需要用一個指標存放資料元素的地址,這樣通過地址就可以找到相關聯資料元素的位置。

鏈式儲存結構

1.2.2 資料結構與演算法關係

資料結構和演算法的關係

演算法是什麼?

演算法就是解決特定問題求解步驟的描述,在計算機中表現為指令的有限序列,並且每個指令表示一個或者多個操作。

演算法有以下特性:

  • 輸入輸出: 演算法具有零個或者多個輸入,至少有一個或多個輸出。
  • 有窮性:有窮性指的是演算法在執行有限的步驟之後,自動結束而不會出現無限迴圈,且每一個步驟都在可接受的時間內完成。
  • 確定性:確定性是指演算法的每一個步驟都具有確定的含義,不能出現二義性。 演算法在一定條件下,只有一條執行路徑,相同的輸入只能有唯一的輸出結果。
  • 可行性:演算法的每一步都必須是可行的,即每一步都能通過執行有限次數完成

演算法設計要求:

  • 正確性:演算法的正確性是指演算法至少應該具有輸入,輸出和加工處理無歧義性,能正確反映問題的需求、能夠得到問題的正確答案。正確性分為4個層次: - 演算法程式沒有語法錯誤; - 演算法程式對於合法的輸入資料能夠產生滿足要求的輸出結果; - 演算法程式對於非法的輸入資料能夠得出滿足規格說明的結果; - 演算法程式對於精心選擇的,甚至刁鑽的測試資料都有滿足要求的輸出結果;
  • 可讀性:: 演算法設計的另一個目的是為了便於閱讀,理解和交流。可讀性高有助於人們理解演算法,晦澀難懂的演算法往往隱含錯誤,不容易發現,並且難於除錯和修改。
  • 健壯性:一個好的演算法還應該能對輸入資料的不合法的情況做出合適的處理,考慮邊界性,也是在寫程式碼經常要做的一個處理。當輸入資料不合法時,演算法也能做出相關處理,而不是產生異常和莫名其妙的結果。
  • 時間效率高和儲存量低:用最少的儲存空間和最少的時間,辦成同樣的事,就是好演算法

1.2.3 時間複雜度和空間複雜度

1.2.3.1 時間複雜度:

使用高階程式語言編寫的程式在計算機上執行時所消耗的時間取決於下列的因素:

  • 演算法採用的策略、方法
  • 編譯產生的程式碼質量
  • 問題的輸入規模
  • 機器執行指令的速度

演算法的時間複雜度的定義如下: 在進行演算法分析時,語句的總執行次數T(n)是關於問題規模n的函式,進而分析T(n)隨著n變化情況並確定T(n)的數量級。演算法的時間複雜度,也就是演算法的時間量度,即為T(n) = O(f(n))。它表示隨問題規模n的增大,演算法執行時間的增長率和f(n)的增長率相同,稱作演算法的漸近時間複雜度,簡稱為時間複雜度。其中f(n)是問題規模n的某個函式。 大寫O( )來體現演算法時間複雜度的記法,我們稱之為大O記法。 推導大O階的方法:

  • 用常數1取代執行時間中所有加法常數

  • 在修改後的執行次數函式中,只保留最高階項

  • 如果在最高階項存在且不是1,則去除與這個項相乘的常數

  • 常數階

int sum = 0, n = 100;
sum = (1 + n) * n / 2;
printf("%d", sum);
複製程式碼

這個演算法的每行程式碼都會執行一次,執行次數函式是f(n) = 3,依據推導大O階的方法,第一步就是將常數替換為1,由於沒有最高階項,所以該演算法的時間複雜度為O(1)。 需要注意的是,常數階的演算法不管常數是多少,我們都記做O(1),並沒有O(2)、O(3)之類的複雜度。

  • 線性階
for (int i = 0; i < n; i++) {
    //  時間複雜度為O(1)的操作   
}
複製程式碼

分析演算法的複雜度,就是要分析迴圈結構的執行情況。上述程式碼的時間複雜度為O(n),是因為迴圈體中的程式碼要執行n次。

  • 對數階
int i = 1;
while (i < n) {
    i = i * 2;
}
複製程式碼

在迴圈體內,i每次都乘以2倍,即為2^x = n,可以得出次數x為以2為低n的對數x = log2n。根據推導大O階的方法,去除最高階項的常數,即為O(logn)。

  • 平方階
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        //  時間複雜度為O(1)的操作   
    }
}
複製程式碼

迴圈巢狀n*n,時間複雜度為O(n^2)。

例如下面函式的執行次數:

求解函式執行次數

上表對應的函式執行次數如下:

函式執行次數結構

時間複雜實際就是評估演算法執行的次數:

時間複雜度比較
O(1) < O(log n) < O(n) < O(nlog n) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

  • 時間複雜度計算練習
#include <stdio.h>
/*大O表示法
 1. 用常數1取代執行時間中所有常數 3->1 O(1)
 2. 在修改執行次數函式中,只保留最高階項 n^3+2n^2+5 -> O(n^3)
 3. 如果在最高階存在且不等於1,則去除這個專案相乘的常數 2n^3 -> n^3
 */

/*
 時間複雜度術語:
 1. 常數階
 2. 線性階
 3. 平方階
 4. 對數階
 5. 立方階
 6. nlog階
 7. 指數階(不考慮) O(2^n)或者O(n!) 除非是非常小的n,否則會造成噩夢般的時間消耗. 這是一種不切實際的演算法時間複雜度. 一般不考慮!
 */

/* 1. 常數階時間複雜度計算 O(1) */
//1+1+1 = 3 O(1)
void testSum1(int n){
    int sum = 0;                //執行1次
    sum = (1+n)*n/2;            //執行1次
    printf("testSum1:%d\n",sum);//執行1次
}

//1+1+1+1+1+1+1 = 7 O(1)
void testSum2(int n){
    int sum = 0;                //執行1次
    sum = (1+n)*n/2;            //執行1次
    sum = (1+n)*n/2;            //執行1次
    sum = (1+n)*n/2;            //執行1次
    sum = (1+n)*n/2;            //執行1次
    sum = (1+n)*n/2;            //執行1次
    printf("testSum2:%d\n",sum);//執行1次
    
}
//x=x+1; 執行1次
void add(int x){
    x = x+1;
}


/*2.線性階時間複雜度*/
//x=x+1; 執行n次 O(n)
void add2(int x,int n){
    for (int i = 0; i < n; i++) {
        x = x+1;
    }
}

//1+(n+1)+n+1 = 3+2n -> O(n)
void testSum3(int n){
    int i,sum = 0;               //執行1次
    for (i = 1; i <= n; i++) {   //執行n+1次
        sum += i;                //執行n次
    }
    printf("testSum3:%d\n",sum);  //執行1次
}

/*3.對數階*/
/*2的x次方等於n x = log2n  ->O(logn)*/
void testA(int n){
    int count = 1;         //執行1次
    //n = 10
    while (count < n) {
        count = count * 2;
    }
    
}


/*4.平方階*/
//x=x+1; 執行n*n次 ->O(n^2)
void add3(int x,int n){
    for (int i = 0; i< n; i++) {
        for (int j = 0; j < n ; j++) {
            x=x+1;
        }
    }
}

//n+(n-1)+(n-2)+...+1 = n(n-1)/2 = n^2/2 + n/2 = O(n^2)
//sn = n(a1+an)/2
void testSum4(int n){
    int sum = 0;
    for(int i = 0; i < n;i++)
        for (int j = i; j < n; j++) {
            sum += j;
        }
    printf("textSum4:%d",sum);
    
}

//1+(n+1)+n(n+1)+n^2+n^2 = 2+3n^2+2n -> O(n^2)
void testSum5(int n){
    int i,j,x=0,sum = 0;           //執行1次
    for (i = 1; i <= n; i++) {     //執行n+1次
        for (j = 1; j <= n; j++) { //執行n(n+1)
            x++;                   //執行n*n次
            sum = sum + x;         //執行n*n次
        }
    }
    printf("testSum5:%d\n",sum);
}


/*5.立方階*/
void testB(int n){
    int sum = 1;                         //執行1次
    for (int i = 0; i < n; i++) {        //執行n次
        for (int j = 0 ; j < n; j++) {   //執行n*n次
            for (int k = 0; k < n; k++) {//執行n*n*n次
                sum = sum * 2;          //執行n*n*n次
            }
        }
    }
}

int main(int argc, const char * argv[]) {
    
    testSum1(100);
    testSum2(100);
    testSum3(100);
    
    return 0;
}
複製程式碼

1.2.3.2 空間複雜度:

演算法的空間複雜度

演算法的空間複雜度通過計算演算法所需要的儲存空間實現,演算法空間複雜度的計算公式記做:S(n) = n(f(n)), 其中n為問題的規模, f(n) 為語句關於n 所佔記憶體空間的函式。

  • 空間複雜度練習
/*
 程式空間計算因素:
 1. 寄存本身的指令
 2. 常數
 3. 變數
 4. 輸入
 5. 對資料進行操作的輔助空間
 
 在考量演算法的空間複雜度,主要考慮演算法執行時所需要的輔助空間.
 空間複雜度計算:

 問題: 陣列逆序,將一維陣列a中的n個數逆序存放在原陣列中.
 */

#include <stdio.h>

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
   
    int n = 5;
    int a[10] = {1,2,3,4,5,6,7,8,9,10};
    
    //演算法實現(1)
    int temp;
    for(int i = 0; i < n/2 ; i++){
        temp = a[i];
        a[i] = a[n-i-1];
        a[n-i-1] = temp;
    }

    for(int i = 0;i < 10;i++)
    {
        printf("%d\n",a[i]);

    }
    
    //演算法實現(2)
    int b[10] = {0};
    for(int i = 0; i < n;i++){
        b[i] = a[n-i-1];
    }
    for(int i = 0; i < n; i++){
        a[i] = b[i];
    }
    for(int i = 0;i < 10;i++)
    {
        printf("%d\n",a[i]);
        
    }
    
    return 0;
}
複製程式碼

1.3 線性表

1.3.1 線性表之順序表

ADT list{ Data:線性表的資料物件集合為{a1,a2,a,......an},每個元素的型別均為DataType,其中,除了第一個元素a1 外,每一個元素有且只有一個直接前驅元素,除了最後一個元素an 外,每個元素有且只有一個直接後續元素,資料元素之間的關係是一對一的關係。 OPeration(操作) InitList(&L) 初始化表 操作結果:初始化操作,建立一個空的線性表L DestroyList(&L) 銷燬表 初始條件:線性表L已存在 操作結果:銷燬線性表L ClearList(&L) 清空表 初始條件:線性表L已存在 操作結果:將L重置為空表 ListEmpty(L) 表是否為空 初始條件:線性表L已存在 操作結果:若L為空表,則返回true,否則返回false ListLength(L) 表長度(元素個數) 初始條件:線性表L已存在 操作結果:返回L中資料元素的個數 ...... GetElem(L,i,&e) 獲取元素 初始條件: 線性表L已存在,且1<=i<ListLength(L) 操作結果: 用e返回L中第i個資料元素的值; LocateElem(L,e) 初始條件: 線性表L已存在 操作結果: 返回L中第1個值與e相同的元素在L中的位置. 若資料不不存在則返回0; PriorElem(L, cur_e,&pre_e); 初始條件: 線性表L已存在 操作結果: 若cur_e是L的資料元素,且不不是第⼀一個,則⽤用pre_e返回其前驅,否則操作失敗. NextElem(L, cur_e,&next_e); 初始條件: 線性表L已存在 操作結果: 若cur_e是L的資料元素,且不不是最後⼀一個,則⽤用next_e返回其後繼,否則操作失敗. ...... ListInsert(L,i,e); 初始條件: 線性表L已存在,且1<=i<=listLength(L) 操作結果: 在L中第i個位置之前插⼊入新的資料元素e,L⻓長度加1. ListDelete(L,i); 初始條件: 線性表L已存在,且1<=i<=listLength(L) 操作結果: 刪除L的第i個元素,L的⻓長度減1. TraverseList(L); 初始條件: 線性表L已存在 操作結果: 對線性表L進⾏行行遍歷,在遍歷的過程中對L的每個結點訪問1次. }ADT List.

1.3.1.1 順序表基本操作實現

順序表完整實現程式碼點選這裡下載:順序表基本操作實現

1.3.1.1.1 順序表結構定義
//順序表結構
typedef struct KSequenceList {
    KElementType *data;
    int length;
}KSList;
複製程式碼
1.3.1.1.2 順序表初始化
// 1 順序表初始化
KStatus initSequenceList(KSList *L) {
    
    //為順序表分配一個大小為MAXSIZE 的陣列空間
    L->data = malloc(sizeof(KElementType) * MAXSIZE);
    //儲存分配失敗直接退出
    if(!L->data) return ERROR;
    //空表長度為0
    L->length = 0;
    
    return OK;
}
複製程式碼
1.3.1.1.3 順序表的插入
// 2 順序表的插入
// 初始條件:順序線性表L已存在,1≤i≤ListLength(L);
// 操作結果:在L中第i個位置之前插入新的資料元素e,L的長度加1
KStatus insertElement(KSList *L, int i, KElementType e) {
    
    //邊界條件判斷
    //1.1 出入的索引 i 合法性判斷, 不能超過連結串列總長度
    if((i < 1) || (i > L->length+1)) return ERROR;
    //1.2 儲存空間是否已滿
    if(L->length == MAXSIZE) return ERROR;
    
    //1.3 插入資料如果不在表尾部,則先往後移動騰出位置給要插入的元素
    if(i <= L->length) {
        for(int j = L->length-1; j >= i-1; j--) {
            //插入位置後面的元素都移動1個位置,讓出第i個位置
            L->data[i+1] = L->data[i];
        }
    }
    
    //1.4 將新元素賦值到騰出的位置,完成插入
    L->data[i-1] = e;
    
    //1.5 連結串列長度增加1
    ++L->length;
    
    return OK;
}
複製程式碼
1.3.1.1.4 順序表的取值
// 3. 順序表的取值
KStatus getElement(KSList L, int i, KElementType *e) {
    
    //邊界條件判斷,i不能超過總長度
    if(i < 1 || i > L.length) return ERROR;
    
    //直接取出第i個元素的值,索引下標對應為i-1 (下標預設從0開始)
    *e = L.data[i-1];
    
    return OK;
}
複製程式碼
1.3.1.1.5 順序表刪除
// 4. 順序表刪除
//初始條件:順序線性表L已存在,1≤i≤ListLength(L)
//操作結果: 刪除L的第i個資料元素,L的長度減1
KStatus deleteElement(KSList *L, int i) {
    
    //邊界條件判斷
    //線性表是否為空
    if(L->length == 0) return ERROR;
    //i值合法性判斷
    if((i < 1) || (i > L->length+1)) return ERROR;
    
    for (int j = i; j < L->length; j++) {
        //被刪除的元素後面的所有元素往前移動一個位置
        L->data[j-1] = L->data[j];
    }
    
    //表長度減1
    L->length--;
    
    return OK;
}
複製程式碼
1.3.1.1.6 清空順序表
// 5. 清空順序表
//初始條件:順序線性表L已存在。操作結果:將L重置為空表
KStatus clearList(KSList *L) {
    L->length = 0;
    return OK;
}
複製程式碼
1.3.1.1.7 判斷順序表清空
// 6. 判斷順序表清空
//初始條件:順序線性表L已存在。操作結果:若L為空表,則返回TRUE,否則返回FALSE
KStatus isListEmpty(KSList L) {
    return L.length == 0 ;
}
複製程式碼
1.3.1.1.8 獲取順序表表長度
// 7. 獲取順序表表長度
int getListLength(KSList L) {
    return L.length;
}

複製程式碼
1.3.1.1.9 遍歷順序表
// 8. 遍歷順序表
//初始條件:順序線性表L已存在
//操作結果:依次對L的每個資料元素輸出
KStatus traverseList(KSList L) {
    for(int i = 0; i < L.length; i++) {
        printf("%d.\n",L.data[i]);
    }
    printf("\n");
    return OK;
}
複製程式碼
1.3.1.1.10 查詢順序表元素下標
// 9. 查詢順序表元素下標
//初始條件:順序線性表L已存在
//操作結果:返回L中第1個與e滿足關係的資料元素的位序。
//若這樣的資料元素不存在,則返回值為0
int getElementIndex(KSList L, KElementType e) {
    //邊界條件判斷
    if (L.length == 0 ) return 0;
    
    //順序查詢
    int i = 0;
    for(i = 0; i < L.length; i++) {
        if (L.data[i] == e) {break;}
    }
    
    if (i >= L.length) return 0;
    
    return i;
}
複製程式碼
1.3.1.1.11 單元測試
// 10. 單元測試
void test() {
    KSList L1;
    //KSList L2;
    KElementType e;
    KStatus iStatus;
    
    //1.1 順序表初始化
    iStatus = initSequenceList(&L1);
    printf("初始化L1後: L.Length = %d\n", L1.length);
    
    //1.2 順序表資料插入
    for(int j=1; j <= 5;j++){
        iStatus = insertElement(&L1, 1, j);
    }
    printf("插入資料L1長度: %d\n",L1.length);
    
    //1.3 順序表取值
    getElement(L1, 5, &e);
    printf("順序表L1第5個元素的值為:%d\n",e);
    
    //1.4 順序表刪除第2個元素
    deleteElement(&L1, 2);
    printf("順序表L1刪除第%d元素,長度為%d\n",2,L1.length);
    
    //1.5 清空順序表
    iStatus = clearList(&L1);
    printf("清空L1後,L.length = %d\n",L1.length);
    
    //1.6 判斷List是否為空
    iStatus = isListEmpty(L1);
    printf("L1是否空:i=%d(1:是 0:否)\n",iStatus);
    
    //1.8 遍歷列印順序表
    for(int j=1; j <= 5;j++){
        iStatus = insertElement(&L1, 1, j);
    }
    traverseList(L1);
}
複製程式碼

輸出結構為:

Hello, World!
初始化L1後: L.Length = 0
插入資料L1長度: 5
順序表L1第5個元素的值為:446
順序表L1刪除第2元素,長度為4
清空L1後,L.length = 0
L1是否空:i=1(1:是 0:否)
5.
0.
0.
446.
446.

Program ended with exit code: 0
複製程式碼

1.3.1.2 線性順序表完整操作程式碼

//
//  main.c
//  001_LinearList
//
//  Created by 孔雨露 on 2020/4/5.
//  Copyright © 2020 Apple. All rights reserved.
//

#include <stdio.h>
#include "stdlib.h"
#include "math.h"
#include "time.h"

#define MAXSIZE 100
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

//KElementType型別根據實際情況而定,這裡假設為int
typedef int KElementType;
//KStatus是函式的型別,其值是函式結果狀態程式碼,如OK等
typedef int KStatus;


//順序表結構
typedef struct KSequenceList {
    KElementType *data;
    int length;
}KSList;

// 1 順序表初始化
KStatus initSequenceList(KSList *L) {
    
    //為順序表分配一個大小為MAXSIZE 的陣列空間
    L->data = malloc(sizeof(KElementType) * MAXSIZE);
    //儲存分配失敗直接退出
    if(!L->data) return ERROR;
    //空表長度為0
    L->length = 0;
    
    return OK;
}

// 2 順序表的插入
// 初始條件:順序線性表L已存在,1≤i≤ListLength(L);
// 操作結果:在L中第i個位置之前插入新的資料元素e,L的長度加1
KStatus insertElement(KSList *L, int i, KElementType e) {
    
    //邊界條件判斷
    //1.1 出入的索引 i 合法性判斷, 不能超過連結串列總長度
    if((i < 1) || (i > L->length+1)) return ERROR;
    //1.2 儲存空間是否已滿
    if(L->length == MAXSIZE) return ERROR;
    
    //1.3 插入資料如果不在表尾部,則先往後移動騰出位置給要插入的元素
    if(i <= L->length) {
        for(int j = L->length-1; j >= i-1; j--) {
            //插入位置後面的元素都移動1個位置,讓出第i個位置
            L->data[i+1] = L->data[i];
        }
    }
    
    //1.4 將新元素賦值到騰出的位置,完成插入
    L->data[i-1] = e;
    
    //1.5 連結串列長度增加1
    ++L->length;
    
    return OK;
}


// 3. 順序表的取值
KStatus getElement(KSList L, int i, KElementType *e) {
    
    //邊界條件判斷,i不能超過總長度
    if(i < 1 || i > L.length) return ERROR;
    
    //直接取出第i個元素的值,索引下標對應為i-1 (下標預設從0開始)
    *e = L.data[i-1];
    
    return OK;
}

// 4. 順序表刪除
//初始條件:順序線性表L已存在,1≤i≤ListLength(L)
//操作結果: 刪除L的第i個資料元素,L的長度減1
KStatus deleteElement(KSList *L, int i) {
    
    //邊界條件判斷
    //線性表是否為空
    if(L->length == 0) return ERROR;
    //i值合法性判斷
    if((i < 1) || (i > L->length+1)) return ERROR;
    
    for (int j = i; j < L->length; j++) {
        //被刪除的元素後面的所有元素往前移動一個位置
        L->data[j-1] = L->data[j];
    }
    
    //表長度減1
    L->length--;
    
    return OK;
}

// 5. 清空順序表
//初始條件:順序線性表L已存在。操作結果:將L重置為空表
KStatus clearList(KSList *L) {
    L->length = 0;
    return OK;
}

// 6. 判斷順序表清空
//初始條件:順序線性表L已存在。操作結果:若L為空表,則返回TRUE,否則返回FALSE
KStatus isListEmpty(KSList L) {
    return L.length == 0 ;
}

// 7. 獲取順序表表長度
int getListLength(KSList L) {
    return L.length;
}

// 8. 遍歷順序表
//初始條件:順序線性表L已存在
//操作結果:依次對L的每個資料元素輸出
KStatus traverseList(KSList L) {
    for(int i = 0; i < L.length; i++) {
        printf("%d.\n",L.data[i]);
    }
    printf("\n");
    return OK;
}

// 9. 查詢順序表元素下標
//初始條件:順序線性表L已存在
//操作結果:返回L中第1個與e滿足關係的資料元素的位序。
//若這樣的資料元素不存在,則返回值為0
int getElementIndex(KSList L, KElementType e) {
    //邊界條件判斷
    if (L.length == 0 ) return 0;
    
    //順序查詢
    int i = 0;
    for(i = 0; i < L.length; i++) {
        if (L.data[i] == e) {break;}
    }
    
    if (i >= L.length) return 0;
    
    return i;
}

// 10. 單元測試
void test() {
    KSList L1;
    //KSList L2;
    KElementType e;
    KStatus iStatus;
    
    //1.1 順序表初始化
    iStatus = initSequenceList(&L1);
    printf("初始化L1後: L.Length = %d\n", L1.length);
    
    //1.2 順序表資料插入
    for(int j=1; j <= 5;j++){
        iStatus = insertElement(&L1, 1, j);
    }
    printf("插入資料L1長度: %d\n",L1.length);
    
    //1.3 順序表取值
    getElement(L1, 5, &e);
    printf("順序表L1第5個元素的值為:%d\n",e);
    
    //1.4 順序表刪除第2個元素
    deleteElement(&L1, 2);
    printf("順序表L1刪除第%d元素,長度為%d\n",2,L1.length);
    
    //1.5 清空順序表
    iStatus = clearList(&L1);
    printf("清空L1後,L.length = %d\n",L1.length);
    
    //1.6 判斷List是否為空
    iStatus = isListEmpty(L1);
    printf("L1是否空:i=%d(1:是 0:否)\n",iStatus);
    
    //1.8 遍歷列印順序表
    for(int j=1; j <= 5;j++){
        iStatus = insertElement(&L1, 1, j);
    }
    traverseList(L1);
}

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    test();
    return 0;
}

複製程式碼

1.3.2 線性表之單連結串列

單連結串列Demo點選這裡下載:單連結串列操作

  • 單連結串列結點

單連結串列結點

//定義結點
typedef struct KNodeInfo{
    KElementType data;
    struct KNodeInfo *next;
}Node;
複製程式碼
  • 單連結串列邏輯狀態

    單連結串列邏輯狀態

  • 增加頭結點的單連結串列邏輯狀態

    增加頭結點的單連結串列邏輯狀態

  • 單連結串列為什麼要增加頭結點

    單連結串列為什麼要增加頭結點

1.3.2.1 單連結串列初始化

//1. 初始化單連結串列
KStatus initList(KLinkList *L) {
    
    //生成頭結點,並使用L指向此頭結點
    *L = (KLinkList)malloc(sizeof(Node));
    //如果分配空間失敗,直接退出
    if(*L == NULL) return ERROR;
    //將頭結點的指標域置為空
    (*L)->next = NULL;
    
    return OK;
}
複製程式碼

1.3.2.2 單連結串列插入結點

  • 單連結串列插入
    單連結串列插入
    實現程式碼:
//2. 單連結串列插入
//初始條件:順序線性表L已存在,1≤i≤ListLength(L);
//操作結果:在L中第i個位置之後插入新的資料元素e,L的長度加1;
KStatus insertElement(KLinkList *L, int i, KElementType e) {
    
    int j = 1;
    KLinkList p, s;
    p = *L;
    
    //尋找第i-1個結點
    while (p && j < i) {
        p = p->next;
        ++j;
    }
    //判斷第i個元素是否存在
    if(!p || j > i) return ERROR;
    
    //生成新結點s
    s = (KLinkList)malloc(sizeof(Node));
    //將e賦值給s的數值域
    s->data = e;
    //將p的後繼結點賦值給s的後繼
    s->next = p->next;
    //將s賦值給p的後繼
    p->next = s;
    
    return OK;
}
複製程式碼

1.3.2.3 單連結串列刪除結點

  • 單連結串列刪除
    單連結串列刪除

實現程式碼:

//4. 單連結串列刪除元素
//初始條件:順序線性表L已存在,1≤i≤ListLength(L)
//操作結果:刪除L的第i個資料元素,並用e返回其值,L的長度減1
KStatus deleteElement(KLinkList *L, int i, KElementType *e) {
    
    int j = 1;
    KLinkList p,q;
    p = (*L)->next;
    
    //查詢第i-1個結點,p指向該結點
    while (p->next && j < (i-1)) {
        p = p->next;
        ++j;
    }
    
    //當i>n 或者 i<i 時,刪除位置不合理
    if (!(p->next) || (j>i-1)) return ERROR;
    
    //q指向要刪除的結點
    q = p->next;
    //將q的後繼賦值給p的後繼
    p->next = q->next;
    //將q結點中的資料賦值給e
    *e = q->data;
    //釋放記憶體
    free(q);
    
    return OK;
}
複製程式碼

1.3.2.4 單連結串列取值

//3. 單連結串列取值
//初始條件: 順序線性表L已存在,1≤i≤ListLength(L);
//操作結果:用e返回L中第i個資料元素的值
KStatus getElement(KLinkList L, int i, KElementType *e) {
    int j = 1;
    //將結點p,指向連結串列L的第一個結點
    KLinkList p = L->next;
    
    //查詢結點,p不為空,且計算j不等於i,則迴圈繼續
    while (p && j < i) {
        p = p->next;
        ++j;
    }
    //如果p為空或者j>i,則 返回error
    if(!p || j > i) return ERROR;
    
    *e = p->data;
    
    return OK;
}
複製程式碼

1.3.2.5 單連結串列建表-頭插法

  • 單連結串列前插法
    單連結串列前插法
    實現程式碼:
//7. 建立單連結串列:頭插入法
//隨機產生n個元素值,建立帶表頭結點的單鏈線性表L(前插法)
void createListByHeadInsert(KLinkList *L, int n) {
    KLinkList p;
    //建立1個帶頭結點的單連結串列
    *L = (KLinkList)malloc(sizeof(Node));
    (*L)->next = NULL;
    
    //迴圈前插入隨機資料
    for (int i = 0; i < n; i++) {
        //生成新結點
        p = (KLinkList)malloc(sizeof(Node));
        //i賦值給新結點
        p->data = i;
        
        //將結點p插入到頭結點之後
        p->next = (*L)->next;
        (*L)->next = p;
    }
}
複製程式碼

1.3.2.6 單連結串列建表-頭插法

  • 單連結串列後插法
    單連結串列後插法
    實現程式碼:
//8. 建立單連結串列:尾部插入法
//隨機產生n個元素值,建立帶表頭結點的單鏈線性表L(後插法)
void createListByTailInsert(KLinkList *L, int n) {
    
    KLinkList p, r;
    
    //建立1個帶頭結點的單連結串列
    *L = (KLinkList)malloc(sizeof(Node));
    (*L)->next = NULL;
    
    //讓r指標指向尾部結點
    r = *L;
    
    //迴圈建立連結串列結點,尾部插入
    for (int i = 0; i < n; i++) {
        //生成新結點
        p = (Node *) malloc(sizeof(Node));
        //賦值隨機數i
        p->data = i;
        
        //尾部插入新結點
        //將表尾終端結點的指標指向新結點
        r->next = p;
        //將當前的新結點定義為表尾終端結點
        r = p;
    }
    
    //將尾部指標next=NULL
    r->next = NULL;
    
}

複製程式碼

1.3.2.7 遍歷單連結串列

//5. 遍歷單連結串列
//初始條件:順序線性表L已存在
//操作結果:依次對L的每個資料元素輸出
KStatus traverseList(KLinkList L) {
    KLinkList p = L->next;
    while (p) {
        printf("%d\n",p->data);
        p = p->next;
    }
    printf("\n");
    return OK;
}
複製程式碼

1.3.2.8 清空單連結串列

//6. 清空單連結串列
//初始條件:順序線性表L已存在。操作結果:將L重置為空表
KStatus clearList(KLinkList *L) {
    
    KLinkList p, q;
    //指向第一個結點
    p = (*L)->next;
    while (p) {
        //遍歷刪除每個結點,並釋放記憶體
        q = p->next;
        free(p);
        p = q;
    }
    //頭結點指標域賦值為空
    (*L)->next = NULL;
    
    return OK;
}
複製程式碼

1.3.2.9 單連結串列操作完整程式碼

//
//  main.c
//  003_LInkedStorage
//
//  Created by 孔雨露 on 2020/4/5.
//  Copyright © 2020 Apple. All rights reserved.
//

#include <stdio.h>
#include "string.h"
#include "ctype.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"

#define ERROR 0
#define TRUE 0
#define FALSE 0
#define OK 1

#define MAXSIZE 20

typedef int KStatus;
typedef int KElementType;

//定義結點
typedef struct KNodeInfo{
    KElementType data;
    struct KNodeInfo *next;
}Node;

typedef struct KNodeInfo *KLinkList;


//1. 初始化單連結串列
KStatus initList(KLinkList *L) {
    
    //生成頭結點,並使用L指向此頭結點
    *L = (KLinkList)malloc(sizeof(Node));
    //如果分配空間失敗,直接退出
    if(*L == NULL) return ERROR;
    //將頭結點的指標域置為空
    (*L)->next = NULL;
    
    return OK;
}

//2. 單連結串列插入
//初始條件:順序線性表L已存在,1≤i≤ListLength(L);
//操作結果:在L中第i個位置之後插入新的資料元素e,L的長度加1;
KStatus insertElement(KLinkList *L, int i, KElementType e) {
    
    int j = 1;
    KLinkList p, s;
    p = *L;
    
    //尋找第i-1個結點
    while (p && j < i) {
        p = p->next;
        ++j;
    }
    //判斷第i個元素是否存在
    if(!p || j > i) return ERROR;
    
    //生成新結點s
    s = (KLinkList)malloc(sizeof(Node));
    //將e賦值給s的數值域
    s->data = e;
    //將p的後繼結點賦值給s的後繼
    s->next = p->next;
    //將s賦值給p的後繼
    p->next = s;
    
    return OK;
}

//3. 單連結串列取值
//初始條件: 順序線性表L已存在,1≤i≤ListLength(L);
//操作結果:用e返回L中第i個資料元素的值
KStatus getElement(KLinkList L, int i, KElementType *e) {
    int j = 1;
    //將結點p,指向連結串列L的第一個結點
    KLinkList p = L->next;
    
    //查詢結點,p不為空,且計算j不等於i,則迴圈繼續
    while (p && j < i) {
        p = p->next;
        ++j;
    }
    //如果p為空或者j>i,則 返回error
    if(!p || j > i) return ERROR;
    
    *e = p->data;
    
    return OK;
}

//4. 單連結串列刪除元素
//初始條件:順序線性表L已存在,1≤i≤ListLength(L)
//操作結果:刪除L的第i個資料元素,並用e返回其值,L的長度減1
KStatus deleteElement(KLinkList *L, int i, KElementType *e) {
    
    int j = 1;
    KLinkList p,q;
    p = (*L)->next;
    
    //查詢第i-1個結點,p指向該結點
    while (p->next && j < (i-1)) {
        p = p->next;
        ++j;
    }
    
    //當i>n 或者 i<i 時,刪除位置不合理
    if (!(p->next) || (j>i-1)) return ERROR;
    
    //q指向要刪除的結點
    q = p->next;
    //將q的後繼賦值給p的後繼
    p->next = q->next;
    //將q結點中的資料賦值給e
    *e = q->data;
    //釋放記憶體
    free(q);
    
    return OK;
}

//5. 遍歷單連結串列
//初始條件:順序線性表L已存在
//操作結果:依次對L的每個資料元素輸出
KStatus traverseList(KLinkList L) {
    KLinkList p = L->next;
    while (p) {
        printf("%d\n",p->data);
        p = p->next;
    }
    printf("\n");
    return OK;
}

//6. 清空單連結串列
//初始條件:順序線性表L已存在。操作結果:將L重置為空表
KStatus clearList(KLinkList *L) {
    
    KLinkList p, q;
    //指向第一個結點
    p = (*L)->next;
    while (p) {
        //遍歷刪除每個結點,並釋放記憶體
        q = p->next;
        free(p);
        p = q;
    }
    //頭結點指標域賦值為空
    (*L)->next = NULL;
    
    return OK;
}

//7. 建立單連結串列:頭插入法
//隨機產生n個元素值,建立帶表頭結點的單鏈線性表L(前插法)
void createListByHeadInsert(KLinkList *L, int n) {
    KLinkList p;
    //建立1個帶頭結點的單連結串列
    *L = (KLinkList)malloc(sizeof(Node));
    (*L)->next = NULL;
    
    //迴圈前插入隨機資料
    for (int i = 0; i < n; i++) {
        //生成新結點
        p = (KLinkList)malloc(sizeof(Node));
        //i賦值給新結點
        p->data = i;
        
        //將結點p插入到頭結點之後
        p->next = (*L)->next;
        (*L)->next = p;
    }
}

//8. 建立單連結串列:尾部插入法
//隨機產生n個元素值,建立帶表頭結點的單鏈線性表L(後插法)
void createListByTailInsert(KLinkList *L, int n) {
    
    KLinkList p, r;
    
    //建立1個帶頭結點的單連結串列
    *L = (KLinkList)malloc(sizeof(Node));
    (*L)->next = NULL;
    
    //讓r指標指向尾部結點
    r = *L;
    
    //迴圈建立連結串列結點,尾部插入
    for (int i = 0; i < n; i++) {
        //生成新結點
        p = (Node *) malloc(sizeof(Node));
        //賦值隨機數i
        p->data = i;
        
        //尾部插入新結點
        //將表尾終端結點的指標指向新結點
        r->next = p;
        //將當前的新結點定義為表尾終端結點
        r = p;
    }
    
    //將尾部指標next=NULL
    r->next = NULL;
    
}

//9. 單元測試
void test() {
        KStatus iStatus;
        KLinkList L;
        KElementType e;
        
        //2.1 單連結串列初始化
        iStatus = initList(&L);
        printf("L 是否初始化成功?(0:失敗,1:成功) %d\n",iStatus);
        
        //2.2 單連結串列插入資料
        for(int j = 1;j<=10;j++)
        {
            iStatus = insertElement(&L, 1, j);
        }
        printf("L 插入後\n");
        traverseList(L);
        
        //2.3 單連結串列獲取元素
        getElement(L,5,&e);
        printf("第5個元素的值為:%d\n",e);
        
        //2.4 刪除第5個元素
        iStatus = deleteElement(&L, 5, &e);
        printf("刪除第5個元素值為:%d\n",e);
        traverseList(L);
        
        //3.1 前插法整理建立連結串列L
        iStatus = clearList(&L);
        createListByHeadInsert(&L, 20);
        printf("整理建立L的元素(前插法):\n");
        traverseList(L);
        
        //3.2 後插法整理建立連結串列L
        iStatus = clearList(&L);
        createListByTailInsert(&L, 20);
        printf("整理建立L的元素(後插法):\n");
        traverseList(L);
}


int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    test();
    return 0;
}

複製程式碼

相關文章