連結串列-雙向通用連結串列

李柱明發表於2020-10-15


前言

  • 20201014
  • 在閱讀 RTOS LiteOS 核心原始碼時發現該核心使用的連結串列是通用連結串列,而 FreeRTOS 核心使用的是非通用連結串列,所以,有必要記錄一下關於連結串列實現的筆記。
  • 以下內容為個人筆記,涉及一些非官方詞彙,敬請諒解,謝謝。

概念

  • 正常表達

    • 連結串列:
      • 連結串列為 C 中一種基礎的資料結構。
      • 看成環形晾衣架即可。
    • 節點:
      • 節點組成連結串列
  • 非通用連結串列自理解概念:節點攜帶資訊

    • 連結串列:圓形的晾衣架
    • 節點:掛鉤
      • 包含上一個
      • 下一個
      • 鉤子等其它需要的資訊
    • 襪子:掛在到 鉤子 的東西
      • 包含被鉤子
      • 襪子攜帶的資訊
  • 通用連結串列自理解概念:資訊攜帶節點

    • 連結串列:圓形的晾衣架
    • 節點:晾衣架圓形框的一截
      • 僅包含上一個
      • 下一個
    • 襪子:擺到晾衣架圓形框的一截上,使得節點成為襪子的一個成員指標變數
      • 襪子攜帶的資訊
      • 資訊中包含節點
  • 通用連結串列與非通用連結串列的區別

    • 通用連結串列節點內容很少一般只有 上一個下一個
    • 通用連結串列節點被放到資訊結構體中,通過偏移找到所在的結構體(即是通過偏移找到襪子頭)
    • 而非通用連結串列是在節點中攜帶資訊結構體的指標的(即是節點就攜帶資訊)。
    • 別人通俗理解,讀者不必理會本小點
      • 通用連結串列是把襪子放到晾衣架的圓形圈上,襪子與圓形圈接觸部分為襪子接待的節點。(資訊攜帶節點
      • 非通用連結串列是。(節點攜帶資訊
      • 通用連結串列的 鏈-線 穿插於襪子中(襪子即是資訊
      • 非通用連結串列的 鏈-線 連在鉤子,再由鉤子鉤襪子

筆錄草稿

雙向連結串列

  • 雙向連結串列理解圖

節點、連結串列及資訊訪問 **

  • 節點
    • 成員僅有是一個和下一個
/*
 *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;
  • 連結串列

    • 多個節點組成連結串列
  • 資訊訪問

    • 操作通用連結串列的最核心、最重要部分是通過偏移獲得資訊控制程式碼襪子頭
      • 如下圖 C 中的長度就是節點與資訊控制程式碼的偏移長度,只需知道 節點地址、資訊型別(結構體型別)及成員名字(即是當前節點在結構體中的成員名字)即可獲得資訊控制程式碼
/*
 * @param item    Current node's pointer.
 * @param type    Structure name of type.
 * @param member  Member name of the doubly linked list in the structure.
 */
#define LSS_LIST_ENTRY(item, type, member)    \
                    ((type *)((char *)(item) - (unsigned long)(&((type *)0)->member)))

操作程式碼及闡述

  • 以下只是通用連結串列的一些擴充套件例子,更多的可以自己象限+實現。

1. 初始化連結串列

  • 上一個指向自己
  • 下一個指向自己
/**
* @brief  連結串列初始化
* @param pstList:需要初始化的連結串列(節點)指標
* @retval none
* @author lzm
*/
void listInit(listItem_t *pstList)
{
    pstList->pstNext = pstList;
    pstList->pstPrev = pstList;
}

2. 獲取第一個節點

  • 指向當前節點的下一個節點
  • 第一個即是下一個
/**
* @brief  獲取第一個節點
* @param pstObject:當前節點指標
* @retval none
* @author lzm
*/
#define listGetFirst(pstObject) ((pstObject)->pstNext)

3. 插入一個節點(頭)

  • 插入當前節點後面
    • 先處理需要插入的節點 外指向
    • 再處理需要插入的節點 內指向
/**
* @brief  插入當前節點後面
* @param pstList:連結串列(也是當前節點)
* @param pstNode:節點(需要插入的節點)
* @retval none
* @author lzm
*/
void listAdd(LSS_LIST *pstList, LSS_LIST *pstNode)
{
    pstNode->pstNext = pstList->pstNext;
    pstNode->pstPrev = pstList;
    pstList->pstNext->pstPrev = pstNode;
    pstList->pstNext = pstNode;
}

4. 插入一個節點(尾)

  • 插入連結串列尾部(即是插入當前節點的前面)
/**
* @brief  插入連結串列尾部
* @param pstList:連結串列(也是當前節點)
* @param pstNode:節點(需要插入的節點)
* @retval none
* @author lzm
*/
void listTailInsert(LSS_LIST *pstList, LSS_LIST *pstNode)
{
    listAdd(pstList->pstPrev, pstNode); // 把當前節點的前一個節點作為參考即可
}

5. 刪除一個節點

  • 刪除當前節點
    • 先處理需要刪除的節點 內指向
    • 再處理需要刪除的節點 外指向
/**
* @brief  刪除當前節點
* @param pstNode:節點(需要刪除的節點)
* @retval none
* @author lzm
*/
void listDelete(LSS_LIST *pstNode)
{
    pstNode->pstNext->pstPrev = pstNode->pstPrev;
    pstNode->pstPrev->pstNext = pstNode->pstNext;
    pstNode->pstNext = (LSS_LIST *)NULL;
    pstNode->pstPrev = (LSS_LIST *)NULL;
}

6. 判斷一個連結串列是否為空

  • 判斷該連結串列節點是否指向 初始化時的值即可。
/**
* @brief  刪除當前節點
* @param pstNode:節點(需要刪除的節點)
* @retval TRUE:連結串列為空
* @retval FALSE:連結串列不為空
* @author lzm
*/
bool listEmpty(LSS_LIST *pstNode)
{
    return (bool)(pstNode->pstNext == pstNode);
}

7. 獲取到資訊控制程式碼的偏移 *

  • 通過 資訊結構體型別、資訊結構體中的成員名字 可以獲得該 名字 到資訊控制程式碼的偏移。
/**
* @brief  獲取到資訊控制程式碼的偏移
* @param type:資訊結構體型別
* @param member:成員名字,即是欄位(域)
* @retval 偏移長度(單位:byte)
* @author lzm
*/
#define getOffsetOfMenber(type, member)    ((uint32_t)&(((type *)0)->member))

8. 獲取節點所在的資訊控制程式碼 *

  • 即是獲取 節點 所在的資訊結構體地址
/**
* @brief  獲取節點所在的資訊控制程式碼
* @param type:資訊結構體型別
* @param member:成員名字,即是欄位(域)
* @retval 返回節點所在的資訊控制程式碼
* @author lzm
*/
#define getItemDataHandle(item, type, member) \
    ((type *)((char *)item - getOffsetOfMenber(type, member))) \

9. 遍歷連結串列

/**
* @brief  刪除節點並重新初始化
* @param pstList:需要重新初始化的連結串列節點
* @retval 
* @author lzm
*/
#define LIST_FOR_EACH(item, list)   \
    for ((item) = (list)->pstNext; \
        (item) != (list); \
        (item) = (item)->pstNext)

10. 遍歷整個連結串列並獲得資訊控制程式碼(巨集) *

  • 本巨集並非為一個完整的語句,僅僅是一個 for 語句,做一個連結串列遍歷。
/**
* @brief 遍歷整個連結串列並獲得資訊控制程式碼(巨集)
* @param handle:儲存目標節點資訊控制程式碼
* @param item:需要遍歷的連結串列(節點)
* @param type:資訊型別(結構體名)
* @param member:該連結串列在 type 中的名字
* @retval 就是也該for語句
* @author lzm
*/
#define LIST_FOR_EACH_HANDEL(handle, list, type, member) \
    for (handle = getItemDataHandle((list)->pstNext, type, member); \
        &handle->member != (list); \
        handle = getItemDataHandle(handle->member.pstNext, type, member))

11. 刪除節點並重新初始化

  • 先從連結串列中刪除本節點
  • 再重新初始化本節點
void osListDel(LSS_LIST *pstPrevNode, LSS_LIST *pstNextNode)
{
    pstNextNode->pstPrev = pstPrevNode;
    pstPrevNode->pstNext = pstNextNode;
}
/**
* @brief  刪除節點並重新初始化
* @param pstList:需要重新初始化的連結串列節點
* @retval 
* @author lzm
*/
void listDelInit(LSS_LIST *pstList)
{
    osListDel(pstList->pstPrev, pstList->pstNext);
    listInit(pstList);
}

參考

相關文章