【資料結構&演算法】04-線性表

李柱明發表於2021-11-04


前言

李柱明部落格:https://www.cnblogs.com/lizhuming/p/15487297.html

線性表的定義

線性表

  • 線性表(list)- 零個或多個資料元素的有限序列。
  • 序列:第一個元素無前驅,最後一個元素無後繼,其他每個元素都有且只有一個前驅和後繼。
  • 有限:元素的個數是有限的。
  • 元素型別:元素型別相同。

線性表的資料型別&操作

線性表操作

  • 線性表的建立和初始化。

  • 清空線性表。

  • 獲取線性表的資料元素。

    • 按位置獲取。
    • 按自定義參考值獲取。
  • 插入資料。

  • 刪除資料。

  • 線性表元素個數。

  • 線性表空判。

  • 線性表滿判。

  • 檢查元素是否存在。

資料型別定義

  • 資料空間。
  • 自定義資料。如鎖、長度記錄等等。
  • 常用操作。
/* 線性表資料結構 */
typedef struct
{
    /* data_typde Data; 資料空間。 */
    /* 自定義資料。 */
    /* 資料操作。 */
}

複雜操作

如合併集合 B 中的資料到集合 A 中:

/* 將所有線上性表 list_b 中但是不在 list_a 中的資料元素插入到 list_a 中 */
void list_union(list *list_a, list *list_b)
{
    int i = 0;
    int list_a_len = 0;
    int list_b_len = 0;
    elem_type elem = 0;

    list_a_len = list_lenght(list_a);
    list_b_len = list_lenght(list_b);

    for(i = 1; i <= list_b_len; i++)
    {
        list_get_elem(list_b, i, &elem); /* 取 list_b 中第 i 個資料元素賦給 elem */
        if(!list_locate_elem(list_a, elem) /* list_a 中不存在和 elem 相同資料元素 */
        {
            list_insert(list_a, ++list_a_len, elem); /* 插入 */
        }
    }
}

線性表的順序儲存結構

線性表的兩種物理結構:

  • 順序儲存結構。
  • 鏈式儲存結構。

順序儲存結構的定義

定義:

  • 線性表的順序儲存結構,指用一段地址連續的儲存單元依次儲存線性表的資料元素。

順序儲存方式

若每個資料型別相同,可以用 C 語言的一維陣列來實現順序儲存結構:

#define MAX_SIZE    /* 資料空間 */
typedef int elem_type; /* 元素型別 */
typedef struct
{
    int length; /* 線性表長度 */
    elem_type data[MAX_SIZE]; /* 實際空間 */
}list_t;

描述順序儲存結構需要三個屬性:

  • 儲存空間的起始位置:陣列 data,它的儲存位置就是儲存空間的儲存位置。
  • 線性表的最大儲存容量:陣列長度 MAX_SIZE。
  • 線性表的當前長度:length。

資料長度和線性表長度的區別

  • 資料長度:是存放線性表的儲存空間的長度,儲存分配後這個量是一般不變的。
  • 線性表長度:線性表中的資料元素的個數。隨著元素的插入、刪除操作的變化而變化。
  • 注意:線性表的長度總是小於等於陣列的長度。

地址的計算方法

地址:儲存器中的每個儲存單元都有自己的編號,這個編號稱為地址。(記憶體地址)

計數:線性表從 1 開始,而 c 語言中的陣列從 0 開始。

locate:獲得儲存位置的函式(假設每個資料元素佔 c 個儲存單元),參考公式:

  • locate(Ai + n) = locate(Ai) + n*c;
  • locate(Ai) = locate(A1) + (i-1)*c;

通過上述公式可以隨時算出線性表中任意位置的地址,且都是相同時間。

由此可知時間複雜度為 O(1)。

每個位置存、取資料,都是相等的時間,也就是一個常數。

通常把具有這一特點的儲存結構稱為 隨機存取結構

順序儲存結構的插入與刪除

注意

  • 線性表的第 i 個數是從 1 開始計數的,而陣列小標是從 0 開始的。
  • 分清應該是 i 還是 i-1。
  • 注意指標丟失問題。

增刪時間複雜度分析:

  • 最好的情況:插入或刪除尾部:O(1)
  • 最壞情況:插入或刪除頭部:O(n)
  • 平均情況:(n-1)/2

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

優點

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

缺點

  • 插入和刪除操作需要搬移大量資料。
  • 當線性表長度變化較大時,難以確定儲存空間的容量。
  • 容易造成儲存空間的碎片。

線性表的鏈式儲存結構

鏈式儲存結構的定義

特點:

  • 用一組任意的儲存單元儲存線性表的資料元素,這組儲存單元可以連續,也可以不連續。(順序儲存要求地址連續)

  • 空間、邏輯負擔:順序結構中,每個資料元素只需要存資料元素的資訊,而鏈式結構中,還需要儲存它的後繼元素的儲存地址。

    • 即是順序儲存結構的前驅後繼是地址相鄰。
    • 而鏈式儲存結構的前驅後繼是靠指標指向。

頭指標與頭結點的異同

頭指標

  • 頭指標是指指向第一個結點的指標。若連結串列有頭結點(哨兵),則是指向頭結點的指標。
  • 頭指標具有表示作用,所以通常當連結串列的控制程式碼。頭指標也冠以連結串列的名字。
  • 無論連結串列是否為空,頭指標都不為空,因為它指向連結串列的實體。
  • 頭指標是連結串列的必要元素。

頭結點

  • 頭結點是為了操作的統一和方便而設立的,就是不用考慮無結點時的連結串列操作。
  • 頭結點放在第一個元素的結點之前,其資料域一般無意義。可以新增一些自定義的資料,用於管理連結串列。可以擴充套件成這個連結串列的控制塊。
  • 頭結點不是連結串列的必要元素。

鏈式儲存結構的資料結構

雙向非通用迴圈連結串列相關節點:

  • 根節點可以擴充套件成連結串列的控制塊。
  • 在節點中掛載資訊。資訊結構可自定義。
/* mini 節點結構體 */
struct LIST_MINI_ITEM_LSS_T
{
    /* 指向,(根據單、雙連結串列刪減以下內容) */
    struct node *pxPrevious; // 上一個節點
    struct node *pxNext; // 下一個節點
     /* 節點屬性,(根據個人需求增減以下內容) */
    uint32_t xItemValue; // 記號值,在雙向連結串列中為最大值
};
typedef struct LIST_MINI_ITEM_LSS_T ListMiniItem_t;

/* 連結串列結構體,即根節點 */
struct LIST_LSS_T
{
    /* 節點屬性,(根據個人需求增減以下內容) \*/
    uint32_t uxNumberOfItems; // 節點數,統計粘附在本連結串列上的節點數
    struct LIST_ITEM_LSS_T * pxIndex; // 索引,連結串列索引,指向連結串列中的某個節點
    struct LIST_MINI_ITEM_LSS_T xListEnd; // 連結串列根節點
};
typedef struct LIST_LSS_T List_t;

/*
 * The linked list node
 */
struct LIST_ITEM_LSS_T
{   
    struct LIST_ITEM_LSS_T * pxNext; // 下一個
    struct LIST_ITEM_LSS_T  * pxPrevious; // 上一個
  
    /* 節點屬性,(根據個人需求增減以下內容) */
    uint32_t xItemValue; // 記號值,一般用於排序
    void * pvOwner; // 掛鉤,即攜帶的資訊
    void * pvContainer; // 歸屬,即屬於哪一個連結串列
};
typedef struct LIST_ITEM_LSS_T listItem_t;

雙向通用迴圈連結串列相關節點:

  • 把節點放置待結構體中即可。
  • 連結串列的每個元素型別一致。
/*
 * Structure of a node in a doubly linked list.
 */
typedef struct LSS_LIST
{
    struct LSS_LIST *pstPrev;            /**< Current node's pointer to the previous node*/
    struct LSS_LIST *pstNext;            /**< Current node's pointer to the next node*/
} LSS_LIST;
typedef struct LSS_LIST listItem_t;

時間複雜度分析

  • 單連結串列的插入和刪除:首先是遍歷查詢第 i 個元素,其次是插入和刪除操作。
  • 所以時間複雜度是 O(n)。
  • 增刪元素時,順序儲存結構需要挪動資料,O(n)。而鏈式儲存結構修改指向的指標 O(1)。
  • 綜上,對於對元素增刪頻繁的,鏈式儲存結構比較好。

連結串列原始碼參考

順序儲存結構與鏈式儲存結構使用建議

  1. 若線性表多查詢,少插刪,宜採用順序儲存結構;若多查刪,宜採用單連結串列結構。
  2. 若線性表的元素個數變化大或不確定時,宜採用單連結串列;若確定或事先明確個數,用順序儲存效率更高。

靜態連結串列

問題:C 語言有指標,python 等物件導向的語言有引用。但是有些語言沒有指標也沒有引用,如何實現連結串列結構?

解決方案:結合陣列&連結串列。用陣列下標代替指標。即是遊標實現法。

靜態連結串列的定義

定義:用陣列描述的連結串列叫做靜態連結串列,或曰“遊標實現法”。

  • 陣列的元素都是由兩個資料域組成的,data(資料)和 cur(遊標)。

  • 陣列的每個下標都對應一個 data 和一個 cur。

    • 資料域 data:用來存放資料元素,也就是我們要處理的資料。
    • 遊標 cur:相當於單連結串列中的 next 指標,存放該元素的後繼在陣列中的下標。
  • 備用連結串列:通常把未被使用的陣列元素稱為備用連結串列。(即陣列的後面還沒填充資料的空閒空間)

  • 兩個特殊位置:

    • 陣列第一個元素空間用於存放備用連結串列的第一個節點。

      • space[0].cur 表示備用連結串列的第一個節點的下標。
      • space[0].cur == (max_size - 1) 時表示備用連結串列已無空間。
    • 陣列最後一個元素空間用於存放頭結點。

      • space[max_size-1].cur 表示第一個有效資料元素的下標。
      • space[max_size-1].cur == 0 時表示資料連結串列為空。

靜態連結串列的資料結構

/* 線性表的靜態連結串列儲存結構 */
typedef struct 
{
    elem_type data;
    int cur;  /* 遊標(Cursor) ,為0時表示無指向 */
} component_t;
component_t static_link_list[MAX_SIZE];

程式碼實現

靜態連結串列的實現

/** @file         static_list.c
 *  @brief        簡要說明
 *  @details      詳細說明
 *  @author       lzm
 *  @date         2021-09-05 22:12:22
 *  @version      v1.0
 *  @copyright    Copyright By lizhuming, All Rights Reserved
 *  @blog         https://www.cnblogs.com/lizhuming/
 *
 **********************************************************
 *  @LOG 修改日誌:
 **********************************************************
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 說明:連結串列操作的索引是 1 起的。 */

/* 線性表的靜態連結串列儲存結構 */
#define MAX_SIZE 100
typedef int elem_type;
typedef struct 
{
    elem_type data;
    int cur;  /* 遊標(Cursor) ,為0時表示無指向 */
}static_list_t;

static_list_t static_list[MAX_SIZE] = {0};

/**
 * @name   static_list_init
 * @brief  將一維陣列space中各分量鏈成一個備用連結串列,space[0].cur為頭指標,"0"表示空指標
 * @param  
 * @retval 
 * @author lzm
 */
int static_list_init(static_list_t *space) 
{
	int i = 0;

    if(space == NULL)
    {
        return -1;
    }

	for (i=0; i<MAX_SIZE-1; i++)  
		space[i].cur = i+1;

	space[MAX_SIZE-1].cur = 0; /* 目前靜態連結串列為空,最後一個元素的cur為0 */

    return 0;
}

/**
 * @name   static_list_malloc
 * @brief  從陣列備用連結串列中模擬 malloc 一個資料空間出來
 * @param  
 * @retval 下標
 * @author lzm
 */
int static_list_malloc(static_list_t *space)
{
    int index = 0;

    if(space == NULL)
        return -1;

    index = space[0].cur;

    if(space[0].cur)
        space[0].cur = space[index].cur;

    return index;
}

/**
 * @name   static_list_free
 * @brief  把一個資料空間放會備用連結串列中
 * @param  
 * @retval 
 * @author lzm
 */
int static_list_free(static_list_t *space, int index)
{
    if(space == NULL || index <= 0 || index >= MAX_SIZE)
        return -1;

    space[index].cur = space[0].cur;
    space[0].cur = index;

    return 0;
}

/**
 * @name   static_list_length
 * @brief  
 * @param  
 * @retval 
 * @author lzm
 */
int static_list_length(static_list_t *space)
{
    int cnt = 0;
    int index = 0;

    if(space == NULL)
        return -1;

    index = space[MAX_SIZE-1].cur;

    while(index)
    {
        cnt++;
        index = space[index].cur;
    }
  
    return cnt;
}

/**
 * @name   static_list_inster
 * @brief  在space資料連結串列中第cur_inster個元素前插入新的元素。(程式設計技巧:找出前一個便好操作了)
 * @param  
 * @retval 
 * @author lzm
 */
int static_list_inster(static_list_t *space, int cur_inster, elem_type elem)
{
    int i = 0;
    int index = 0;
    int index_m = 0;

    if(space == NULL || cur_inster < 1 || cur_inster > (static_list_length(space) + 1))
        return -1;

    index_m = static_list_malloc(space);
    if(index_m <= 0)
        return -1;

    space[index_m].data = elem;

    index = MAX_SIZE-1;
    for(i = 1; i < cur_inster; i++)
        index = space[index].cur;
  
    space[index_m].cur = space[index].cur;
    space[index].cur = index_m;

    return 0;
}

/**
 * @name   static_list_delete
 * @brief  在space資料連結串列中刪除第cur_delete個元素。
 * @param  
 * @retval 
 * @author lzm
 */
int static_list_delete(static_list_t *space, int cur_delete)
{
    int i = 0;
    int index = 0;
    int index_d = 0;

    if(space == NULL)
        return -1;

    if(space == NULL || cur_delete < 1 || cur_delete > static_list_length(space))
        return -1;

    index = MAX_SIZE - 1;

    for(i = 1; i < cur_delete; i++) // 獲取實際需要刪除的前一個
        index = space[index].cur;
  
    index_d = space[index].cur;
    space[index].cur = space[index_d].cur;

    static_list_free(space, index_d);

    return 0;
}

相關文章