數據結構學習總結(1)線性表之順序表


    通過前面的學習知道,具有“一對一”邏輯關系的數據,最佳的存儲方式是使用線性表。那么,什么是線性表呢?

線性表,全名為 線性存儲結構。使用線性表存儲數據的方式可以這樣理解,即“把所有數據用一根線兒 起來,再存儲到物理空間中”。
采用線性表將其儲存到物理空間中。
    首先,用“一根線兒”把它們按照順序“串”起來,如圖 2 所示:  
            
                                                            圖 2 數據的"線性"結構
圖 2 中,左側是“串”起來的數據,右側是空閑的物理空間。把這“一串兒”數據放置到物理空間,我們可以選擇以下兩種方式,如圖 3 所示
 
             兩種線性存儲結構
                  圖 3 兩種線性存儲結構
  圖 3a) 是多數人想到的存儲方式,而圖 3b) 卻少有人想到。我們知道,數據存儲的成功與否,取決於是否能將數據完整地復原成它本來的樣子。如果把圖 3a) 和圖 3b) 線的一頭扯起,你會發現數據的位置依舊沒有發生改變。因此可以認定,這兩種存儲方式都是正確的。
  將具有“一對一”關系的數據“線性”地存儲到物理空間中,這種存儲結構就稱為線性存儲結構(簡稱線性表)。
  使用線性表存儲的數據,如同向 數組中存儲數據那樣,要求數據類型必須一致,也就是說,線性表存儲的數據,要么全不都是整形,要么全部都是字符串。一半是整形,另一半是字符串的一組數據無法使用線性表存儲。
  

順序存儲結構和鏈式存儲結構

圖 3 中我們可以看出,線性表存儲數據可細分為以下 2 種:
  1. 如圖 3a) 所示,將數據依次存儲在連續的整塊物理空間中,這種存儲結構稱為順序存儲結構(簡稱順序表);
  2. 如圖 3b) 所示,數據分散的存儲在物理空間中,通過一根線保存着它們之間的邏輯關系,這種存儲結構稱為鏈式存儲結構(簡稱鏈表);
也就是說,線性表存儲結構可細分為順序存儲結構和鏈式存儲結構。

前驅和后繼

數據結構中,一組數據中的每個個體被稱為“數據元素”(簡稱“元素”)。例如,圖 1 顯示的這組數據,其中 1、2、3、4 和 5 都是這組數據鍾的一個元素。

另外,對於具有“一對一”邏輯關系的數據,我們一直在用“某一元素的左側(前邊)或右側(后邊)”這樣不專業的詞,其實線性表中有更准確的術語:
  • 某一元素的左側相鄰元素稱為“直接前驅”,位於此元素左側的所有元素都統稱為“前驅元素”;
  • 某一元素的右側相鄰元素稱為“直接后繼”,位於此元素右側的所有元素都統稱為“后繼元素”;
以圖 1 數據中的元素 3 來說,它的直接前驅是 2 ,此元素的前驅元素有 2 個,分別是 1 和 2;同理,此元素的直接后繼是 4 ,后繼元素也有 2 個,分別是 4 和 5。如圖 4 所示:

           前驅和后繼
                    圖 4 前驅和后繼
  順序表,全名順序存儲結構,是 線性表的一種,線性表用於存儲邏輯關系為“一對一”的數據,順序表自然也不例外。

不僅如此,順序表對數據的物理存儲結構也有要求。順序表存儲數據時,會提前申請一整塊足夠大小的物理空間,然后將數據依次存儲起來,存儲時做到數據元素之間不留一絲縫隙。

例如,使用順序表存儲集合  {1,2,3,4,5},數據最終的存儲狀態如  1 所示:

          
                  圖 1 順序存儲結構示意圖

由此我們可以得出,將“具有 '一對一' 邏輯關系的數據按照次序連續存儲到一整塊物理空間上”的存儲結構就是順序存儲結構。
通過觀察圖 1 中數據的存儲狀態,我們可以發現,順序表存儲數據同 數組非常接近。其實,順序表存儲數據使用的就是數組。

順序表的初始化

使用順序表存儲數據之前,除了要申請足夠大小的物理空間之外,為了方便后期使用表中的數據,順序表還需要實時記錄以下 2 項數據:
  1. 順序表申請的存儲容量;
  2. 順序表的長度,也就是表中存儲數據元素的個數;
提示:正常狀態下,順序表申請的存儲容量要大於順序表的長度。
因此,我們需要自定義順序表,C 語言實現代碼如下
typedef struct Table{
    int * head;//聲明了一個名為head的長度不確定的數組,也叫“動態數組”
    int length;//記錄當前順序表的長度
    int size;//記錄順序表分配的存儲容量
}table;

注意,head 是我們聲明的一個未初始化的動態數組,不要只把它看做是普通的指針。

接下來開始學習順序表的初始化,也就是初步建立一個順序表。建立順序表需要做如下工作:

  • 給 head 動態數據申請足夠大小的物理空間;
  • 給 size 和 length 賦初值;

因此,C 語言實現代碼如下:

#define Size 5 //對Size進行宏定義,表示順序表申請空間的大小
table initTable(){
    table t;
    t.head=(int*)malloc(Size*sizeof(int));//構造一個空的順序表,動態申請存儲空間
    if (!t.head) //如果申請失敗,作出提示並直接退出程序
    {
        printf("初始化失敗");
        exit(0);
    }
    t.length=0;//空表的長度初始化為0
    t.size=Size;//空表的初始存儲空間為Size
    return t;
}

我們看到,整個順序表初始化的過程被封裝到了一個函數中,此函數返回值是一個已經初始化完成的順序表。這樣做的好處是增加了代碼的可用性,也更加美觀。與此同時,順序表初始化過程中,要注意對物理空間的申請進行判斷,對申請失敗的情況進行處理,這里只進行了“輸出提示信息和強制退出”的操作,可以根據你自己的需要對代碼中的 if 語句進行改進。

通過在主函數中調用 initTable 語句,就可以成功創建一個空的順序表,與此同時我們還可以試着向順序表中添加一些元素,C 語言實現代碼如下:

#include<stdio.h>
#include<stdlib.h>
#define Size 5
//定義一個順序表
typedef struct table{
    int *head;//用於定義數組頭地址
    int length;//記錄當前順序表的長度
    int size;//記錄順序表的存儲容量
}table;
 //初始化順序表
table initTable() {
    table t;
    t.head = (int*)malloc(Size * sizeof(int));//動態申請存儲空間
    if (!t.head) {
        printf("順序表初始化是被失敗");
        exit(0);
    }
    t.length = 0;//空表長度初始化為0
    t.size = Size;//空表可以存儲元素個數
    return t;
}
//輸出順序表中元素函數
void display(table t) {
    for (int i = 0; i < t.length;i++) {
        printf("%d ",t.head[i]);
    }
    printf("\n");
}
int main() { 
     table t = initTable();
    //向順序表中添加元素
    for (int i = 1; i <= Size;i++) {
        t.head[i - 1] = i;
        t.length++;
    }
    printf("順序表中存儲元素分別是:\n");
    display(t);
    return 0;
}

程序運行結果如下:

順序表中存儲的元素分別是:
1 2 3 4 5
可以看到,順序表初始化成功。

順序表插入元素

向已有順序表中插入數據元素,根據插入位置的不同,可分為以下 3 種情況:
  1. 插入到順序表的表頭;
  2. 在表的中間位置插入元素;
  3. 尾隨順序表中已有元素,作為順序表中的最后一個元素;
雖然數據元素插入順序表中的位置有所不同,但是都使用的是同一種方式去解決,即:通過遍歷,找到數據元素要插入的位置,然后做如下兩步工作:
  • 將要插入位置元素以及后續的元素整體向后移動一個位置;
  • 將元素放到騰出來的位置上;
例如,在  {1,2,3,4,5} 的第 3 個位置上插入元素 6,實現過程如下:
  • 遍歷至順序表存儲第 3 個數據元素的位置,如 1 所示:
           找到目標元素位置
                  圖 1 找到目標元素位置
  • 將元素 3 以及后續元素 4 和 5 整體向后移動一個位置,如圖 2 所示:

           將插入位置騰出
                    圖 2 將插入位置騰出
  • 將新元素 6 放入騰出的位置,如圖 3 所示:

           插入目標元素
                    圖 3 插入目標元素
因此,順序表插入數據元素的 C 語言實現代碼如下:    
//插入函數,其中,elem為插入的元素,add為插入到順序表的位置
table addTable(table t,int elem,int add)
{
    //判斷插入本身是否存在問題(如果插入元素位置比整張表的長度+1還大(如果相等,是尾隨的情況),或者插入的位置本身不存在,程序作為提示並自動退出)
    if (add>t.length+1||add<1) {
        printf("插入位置有問題");
        return t;
    }
    //做插入操作時,首先需要看順序表是否有多余的存儲空間提供給插入的元素,如果沒有,需要申請
    if (t.length==t.size) {
        t.head=(int *)realloc(t.head, (t.size+1)*sizeof(int));
        if (!t.head) {
            printf("存儲分配失敗");
            return t;
        }
        t.size+=1;
    } 
    //插入操作,需要將從插入位置開始的后續元素,逐個后移
    for (int i=t.length-1; i>=add-1; i--) {
        t.head[i+1]=t.head[i];
    }
    //后移完成后,直接將所需插入元素,添加到順序表的相應位置
    t.head[add-1]=elem;
    //由於添加了元素,所以長度+1
    t.length++;
    return t;
}

注意,動態數組額外申請更多物理空間使用的是 realloc 函數。並且,在實現后續元素整體后移的過程,目標位置其實是有數據的,還是 3,只是下一步新插入元素時會把舊元素直接覆蓋。

順序表刪除元素

從順序表中刪除指定元素,實現起來非常簡單,只需找到目標元素,並將其后續所有元素整體前移 1 個位置即可。       
 后續元素整體前移一個位置,會直接將目標元素刪除,可間接實現刪除元素的目的。
 例如,從  {1,2,3,4,5} 中刪除元素 3 的過程如圖 4 所示:

        
              圖 4 順序表刪除元素的過程示意圖

因此,順序表刪除元素的 C 語言實現代碼為:
//刪除指定元素
table deltable(table t,int del) {
    //add表示順序表中刪除元素的位置
    if (del>t.length|| del<1) {
        printf("被刪除的元素有誤");
        exit(0);
    }
    //刪除操作
    for (int i = del; i < t.length;i++) {
        t.head[i - 1] = t.head[i];
    }
    t.length--;
    return t;
}

順序表查找元素

順序表中查找目標元素,可以使用多種查找算法實現,比如說二分查找算法、插值查找算法等。
這里,我們選擇順序查找算法,具體實現代碼為:

//查找函數,其中,elem表示要查找的數據元素的值
int selectTable(table t,int elem){
    for (int i=0; i<t.length; i++) {
        if (t.head[i]==elem) {
            return i+1;
        }
    }
    return -1;//如果查找失敗,返回-1
}

順序表更改元素

順序表更改元素的實現過程是:

  1. 找到目標元素;
  2. 直接修改該元素的值;

順序表更改元素的 C 語言實現代碼為

//更改函數,其中,elem為要更改的元素,newElem為新的數據元素
table amendTable(table t,int elem,int newElem){
    int add=selectTable(t, elem);
    t.head[add-1]=newElem;//由於返回的是元素在順序表中的位置,所以-1就是該元素在數組中的下標
    return t;
}

以上是順序表使用過程中最常用的基本操作,這里給出本節完整的實現代碼:

#include<stdio.h>
#include<stdlib.h>
#define Size 5
//定義一個順序表
typedef struct table{
    int *head;//用於定義數組頭地址
    int length;//記錄當前順序表的長度
    int size;//記錄順序表的存儲容量
}table;
 //初始化順序表
table initTable() {
    table t;
    t.head = (int*)malloc(Size * sizeof(int));//動態申請存儲空間
    if (!t.head) {
        printf("順序表初始化是被失敗");
        exit(0);
    }
    t.length = 0;//空表長度初始化為0
    t.size = Size;//空表可以存儲元素個數
    return t;
}
//插入函數,elem:插入的元素   add:插入到順序表的位置
table addTable(table t, int elem,int add) {
     /*1,判斷插入本身是否存在問題
       如果插入元素位置比整張表的長度+1還大,(如果相等,就是尾隨的情況)
       或者插入位置本身不存在程序作為提示,自動退出*/
    if (add > t.length + 1 || add < 1) {
        printf("插入位置有問題");
        return t;
    }
    /*做插入時,首先要看順序表是否有多余的存儲空間提供給插入的元素,
    如果沒有需要申請
    */
    if (t.length==t.size) {
        t.head = (int*)realloc(t.head,(t.size+1)*sizeof(int));
        if (!t.head) {
            printf("存儲分配失敗");
            exit(0);
        }
        t.size += 1;
    }
    //插入操作,需要從插入位置開始的后續元素,逐個后移
    for (int i = t.length - 1; i >= add - 1;i--) {
        t.head[i + 1] = t.head[i];
    }
    //后移完成后直接將所需元素,添加到順序表相應的位置
    t.head[add - 1] = elem;
    //由於添加了元素,所以長度+1
    t.length++;
    return t;     
}
//刪除指定元素
table deltable(table t,int del) {
    //add表示順序表中刪除元素的位置
    if (del>t.length|| del<1) {
        printf("被刪除的元素有誤");
        exit(0);
    }
    //刪除操作
    for (int i = del; i < t.length;i++) {
        t.head[i - 1] = t.head[i];
    }
    t.length--;
    return t;
}
//查找元素,elem表示要找的數據元素的值
int findByElem(table t,int elem) {
    for(int i=0;i < t.length;i++){
        if (t.head[i]==elem) {
            return i + 1;
        }
    }
    return -1;//如果查找失敗返回-1
}
//更改順序表中某個元素
table updateTable(table t,int oldElem,int newElem) {
    int n = findByElem(t,oldElem);//找出old元素所在位置
    t.head[n - 1] = newElem;//由於返回的是元素在順序表中的位置,所以-1就是元素在數組的下標
    return t;
}

//輸出順序表中元素函數
void display(table t) {
    for (int i = 0; i < t.length;i++) {
        printf("%d ",t.head[i]);
    }
    printf("\n");
}
int main() { 
     table t = initTable();
    //向順序表中添加元素
    for (int i = 1; i <= Size;i++) {
        t.head[i - 1] = i;
        t.length++;
    } 
    printf("原順序表中存儲元素順序是:\n");
    display(t); 

    table t1=addTable(t,8,2);
    printf("添加元素后是:\n");
    display(t1);
    t1=deltable(t1,2);
    printf("刪除add位上的元素后:\n");
    display(t1);

    int n=findByElem(t1,4);
    printf("查找元素4的位置:%d \n",n);

    t1 = updateTable(t1,1,9);
    printf("元素1修改為9后遍歷:\n");
    display(t1);
    return 0;
}
View Code

程序運行結果為:

 總結:

順序表的短板

插入元素,時間復雜度 O(n)

插入為 第 i  個 元素,則需要移動  n - i +1  個數據元素. 需要移動 第 n  到第 i 個 元素。
均值的計算:  一共為   (n+1)(n+0)  / 2 ,因為一共計算插了 n+1 個位置。則均值為 :  n / 2
  
刪除元素,時間復雜度 O(n)
 
刪除 第 i  個 元素,則需要移動 n - i  個數據元素 。 需要移動 第   i+ 1  到 第  n 個 元素。
均值的計算:一共為   (n)(n-1+0)  / 2 ,因為一共計算刪除 n  個位置。則均值為 :  (n-1) / 2
        

小提示

1、一般在實際開發時,為了盡量避免移動元素的開銷,都會使用貼近硬件的API去完成內存數據的移動,而不是使用循環。例如使用memmove函數。
2、當內部數組的容量不夠時,需要重新調整數組的大小,上面的例子我們使用了realloc函數去實現,且每次增加20。然而我們必須認識到,調整大小是很銷耗資源的一個操作,因此在實際開發時,我們必須做出明智的容量增長策略。例如:Java中的ArrayList每次將容量擴展為原來的1.5倍。

          

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM