• 线性表是存储数据中具有一对一关系的一种存储结构。
  • 注意这里的线性表指的是存储结构(顺序表、链表),而不是存取结构(栈、队列等)。存取结构也可以基于存储结构来实现,比如顺序栈链栈
  • 线性存储结构共同点是将数据按照顺序用一根线串联起来。
  • 所存储的数据类型必须相同。
    • 对于顺序表而言。每一个结点元素的类型都是相同的,因为需要通过索引来找到元素的位置,则每个结点元素的长度必须相同,符合相同结构体定义的数据才能被线性表的统一规则正确访问。
    • 对于链表而言。链表对结点的访问依赖于指针而不依赖于索引,即是不依赖于结点的相同性。但归根结底,链表也是数据存储结构,对结点的访问终究是要访问到具体的数据。两个结点如果属于不同的结构体,那么通过结构体变量访问数据项的时候,不同的数据类型所占的长度也是不一样的,无法通过统一规则访问。所以链表也要求数据类型必须相同。

1 顺序表

  • 通过元素的顺序存储来反映数据间的逻辑关系。
  • 可以通过索引随机存取顺序表的元素。
    • 索引访问的优势。定义顺序存储结构时,结点首地址结点大小是已知的,借助于地址空间的连续性,拿到索引(偏移量)就可以精确计算出访问元素的位置。因此更适合查询更新元素多的场景。时间复杂度O(1)
    • 索引访问的不足。所有的插入删除操作需要通过移动元素来实现,因为需要保证存储结构所定义的地址空间连续性,元素通过索引访问的正确性。移动元素是一个非常耗性能的操作。若顺序的长度为n,那么平均时间复杂度O(n/2)。
  • 存储结构定义。
    • 由于顺序表是顺序存储,且通过索引访问元素,则用一个数组来存储它的元素。
    • 涉及到插入元素可能超过数组容量,所以需要实现动态数组的功能。
      • 指针来存储数组首地址
      • 用一个length字段来存储当前元素个数
      • 动态数组最大容量,是判断是否需要扩容的依据。
      • 达到扩容条件时候,每次扩容需要增量。 ```c // 定义初始大小和增量

        define LIST_INIT_SIZE 150

        define LIST_INCREMENT 20

        // 定义返回状态及返回码

        define Status int

        define OK 1

        define ERROR 0

        // 定义存储的元素类型

        define ElementType int

// 顺序表的存储结构定义 typedef struct { // 线性表存储空间地址 ElementType* elem;

  1. // 线性表当前存放元素的个数
  2. int length;
  3. // 当前线性表的容量
  4. int listSize;
  5. // 约定每次扩容的增量
  6. int incrementSize;

}SqList;

  1. - 常用操作注意项。
  2. - **初始化** -> 分配内存需要注意失败的情况。
  3. ```c
  4. /* 01_顺序表——创建空表:没有元素,长度为0,默认容量和增量*/
  5. Status createList_Sq(SqList& sL, int initSize = LIST_INIT_SIZE, int incrementSize = LIST_INCREMENT)
  6. {
  7. // 开辟顺序表存储空间
  8. sL.elem = (ElementType*)malloc(initSize * sizeof(ElementType));
  9. // 创建失败,返回错误码
  10. if (!sL.elem)
  11. {
  12. return ERROR;
  13. }
  14. sL.length = 0;
  15. sL.listSize = initSize;
  16. sL.incrementSize = incrementSize;
  17. return OK;
  18. }
  • 插入元素

    1. /* 05_顺序表——插入元素:在指定索引 index 之前插入一个元素*/
    2. Status insertElem_Sq(SqList& sL, int index, ElementType elem)
    3. {
    4. // 索引不合法
    5. if (index < 0)
    6. {
    7. return ERROR;
    8. }
    9. // 是否进行扩容
    10. if (sL.length + 1 >= sL.listSize)
    11. {
    12. sL.elem = (ElementType*)malloc((sL.listSize + (size_t)sL.incrementSize) * sizeof(ElementType));
    13. // 扩容失败
    14. if (!sL.elem)
    15. {
    16. return ERROR;
    17. }
    18. sL.listSize += sL.incrementSize;
    19. }
    20. // 在末尾添加
    21. if (index >= sL.length)
    22. {
    23. //sL.elem[sL.length] = elem;
    24. *(sL.elem + sL.length) = elem;
    25. }
    26. else
    27. {
    28. // 插入前从后往前移动元素,注意索引取值
    29. /*for (int i = sL.length - 1; i >= index; i--)
    30. {
    31. sL.elem[i + 1] = sL.elem[i];
    32. }*/
    33. for (ElementType* p = sL.elem + sL.length + 1; p >= sL.elem + index; p--)
    34. {
    35. *(p + 1) = *p;
    36. }
    37. //sL.elem[index] = elem;
    38. *(sL.elem + index) = elem;
    39. }
    40. // 不要忘记长度
    41. ++sL.length;
    42. return OK;
    43. }
    • 插入索引在当前长度范围外的定义。
    • 注意判断是否达到扩容条件,以及扩容失败后的操作。
    • 插入成功后不要忘记当前元素长度字段。
  • 删除元素 -> 同插入。只是少了扩容条件,多了返回元素的功能性定义。

    1. /* 08_顺序表——删除:删除指定索引的元素*/
    2. ElementType deleteElem_Sq(SqList& sL, int index)
    3. {
    4. // 如果不在元素索引范围内
    5. if (index < 0 || index >= sL.length)
    6. {
    7. return -1;
    8. }
    9. ElementType toDelete = *(sL.elem + index);
    10. // 数组方式
    11. /*for (int i = index; i < sL.length - 1; i++)
    12. {
    13. sL.elem[i] = sL.elem[i + 1];
    14. }*/
    15. // 指针方式
    16. ElementType* p = sL.elem + index;
    17. ElementType* q = sL.elem + sL.length - 1;
    18. while (p < q)
    19. {
    20. *p = *(p + 1);
    21. p++;
    22. }
    23. // 别忘记长度减一
    24. sL.length--;
    25. return toDelete;
    26. }
  • 清空和销毁。

    • 清空是置为初始化状态,存储结构定义还在。

      1. /* 06_顺序表——清空*/
      2. Status clearList_Sq(SqList& sL)
      3. {
      4. // 先清空数组中的值
      5. delete[] sL.elem;
      6. // 重新分配
      7. sL.elem = (ElementType*)malloc(LIST_INIT_SIZE * sizeof(ElementType));
      8. if (!sL.elem)
      9. {
      10. return ERROR;
      11. }
      12. // 其余长度置为初始
      13. sL.length = 0;
      14. sL.listSize = LIST_INIT_SIZE;
      15. sL.incrementSize = LIST_INCREMENT;
      16. return OK;
      17. }
    • 销毁需要释放空间。

      1. /* 07_顺序表——销毁*/
      2. Status destroyList_Sq(SqList& sL)
      3. {
      4. // 回收数组空间
      5. delete[] sL.elem;
      6. //free(sL.elem);
      7. // 其余控制元素置为0
      8. sL.length = 0;
      9. sL.listSize = 0;
      10. sL.incrementSize = 0;
      11. return OK;
      12. }

2 链表

  • 通过链式存储来反映元素之间的一对一关系。
  • 通过指针构成的链遍历链表的元素。
    • 链式存储的优势。对于插入和删除操作,只需要针对插入/删除元素与其相邻的元素进行操作,不存在大量移动元素的耗性能的情景。
    • 链式存储的不足。对于更新查询操作,需要从表头指针开始向后访问,即是以遍历的方式进行顺序访问,访问不方便。

主要通过单链表来记录链式存储结构的定义和常用操作,再此基础上类比循环链表和双向链表。

2.1 单链表

  • 存储结构定义
    • 对于数据而言。数据是零散存储的,每个元素结点只需要维护该结点的元素。
    • 对于数据间的一对一关系,需要指针进行维护。所以需定义指向当前元素结点类型的指针,完成对链的定义。
    • 对于链表结构,初始访问地址需要一个指针,拿到它才能对该链表进行操作。这里将这个指针的存储位置不同,将单链表分为带头结点和不带头结点。
      • 带头结点的单链表。即是以下结构体后的HNode类型。
        • 数据域的ElementType一般不用,或是维护该链表信息用,比如当前元素个数等。
        • 指针域 next指向真正存储的第一个结点元素,即首元结点。
      • 不带头节点的单链表。即是以下结构中的*SL,指向的元素即为第一个元素(首元)。 ```c // 定义返回状态及返回码

        define Status int

        define OK 1

        define ERROR 0

        // 定义存储的元素类型

        define ElementType int

/ 定义单链表结点/ typedef struct SL_Node { // 存储的元素 ElementType data; // 指向下一个结点的指针 struct SL_Node next; }HNode, SL;

  1. > <a name="58d31dee"></a>
  2. ### 常用操作注意项
  3. <a name="465f8828"></a>
  4. #### 2.1.1 初始化操作
  5. - 带头结点。即是需要构造出一个头节点,并且返回或初始化该头节点的地址作为头指针。有两种方式创建头节点。
  6. - 通过指针的创建。由以上定义,SL指向该类型结点的指针。此处head即是**指向头结点类型的指针**。由于指针在定义的时候,我们是不知道该头指针的地址的,当它分配完内存后我们才能得知地址,涉及的操作需要**修改**指向头结点的**head指针的值**,我们必须将头指针作为**引用类型传入**,是得修改能对该变量生效。
  7. ```c
  8. /* 01_单链表——初始化_带头结点
  9. */
  10. Status initList_SLh(SL& head)
  11. {
  12. // 指向头节点的指针
  13. head = (SL)malloc(sizeof(SL_Node));
  14. if (!head)
  15. {
  16. return ERROR;
  17. }
  18. head->data = NULL;
  19. head->next = NULL;
  20. return OK;
  21. }
  • 通过指向头节点的二级指针创建。通过以上指针方式创建的讲述,应该会更好理解指向结点的二级指针方式创建头结点。

    1. /* 18_单链表——初始化_带头结点2*/
    2. Status initList_SLh2(HNode** hNode)
    3. {
    4. *hNode = (HNode*)malloc(sizeof(SL_Node));
    5. if (!*hNode)
    6. {
    7. return ERROR;
    8. }
    9. (*hNode)->data = NULL;
    10. (*hNode)->next = NULL;
    11. return OK;
    12. }
  • 同顺序表,两种方式内存分配都需要考虑分配失败的情况。

    • 不带头结点。即是需要初始化一个指向首元的指针,初始化时候没有元素,所以指针给一个NULL值即可。
      1. /* 02_单链表——初始化_无头结点*/
      2. Status initList_SL(SL& sL)
      3. {
      4. sL = NULL;
      5. return OK;
      6. }

2.1.2 查询、更新、求长度、遍历操作

需要从第一个元素依次往后查找。区分带头结点和不带头节点,这里举出求长度的操作。

  • 带头结点。头指针指向的是头节点,首元是头节点的next域指向的结点。

    1. /* 03_单链表——长度_带头结点*/
    2. int listLength_SLh(SL head)
    3. {
    4. // 初始迭代长度
    5. int length = 0;
    6. // 指向头结点的变量
    7. SL p = head->next;
    8. while (p)
    9. {
    10. length++;
    11. p = p->next;
    12. }
    13. return length;
    14. }
  • 不带头节点。头指针指向的即是首元结点。

    1. /* 04_单链表——长度_无头结点*/
    2. int listLength_SL(SL sL)
    3. {
    4. int length = 0;
    5. // 从头指针开始迭代
    6. SL p = sL;
    7. while (p)
    8. {
    9. length++;
    10. p = p->next;
    11. }
    12. return length;
    13. }

2.1.3 插入元素方式

这里采用批量插入方式作为说明,对应的单个元素插入方式即为后插法、前插法。 同样,插入元素需要判断内存分配结果。

1 尾插法

这里是用于批量建立的过程。 整个过程只需要两个指针(待插入元素的指针s;其插入后的前驱位置元素指针p)

  • 类比为单个元素的后插法,与前插法比较,后插法不需要额外的指针。两个操作的顺序不可逆,可画图理解。
  1. // 1. 将【待插入元素】指向【前驱元素】的后继
  2. s->next = p -> next;
  3. // 2. 将【前驱元素】指向【待插入元素】
  4. p->next = s;
  • 带头结点

    1. /* 07_单链表——尾插法建立_带头节点*/
    2. Status tailInsert_SLh(SL& head, ElementType *datas, int length)
    3. {
    4. // 指针p存放上一个结点
    5. SL p = head;
    6. // 用于新建的结点
    7. SL s = NULL;
    8. for (int i = 0; i < length; i++)
    9. {
    10. // 指针p,每次循环在最后一个结点指针域,进行结点创建
    11. s = (SL)malloc(sizeof(SL_Node));
    12. if (!s)
    13. {
    14. return ERROR;
    15. }
    16. s->data = *(datas + i);
    17. s->next = NULL;
    18. // 新结点连接到上一个结点
    19. p->next = s;
    20. // 迭代
    21. p = s;
    22. s = s->next;
    23. }
    24. return OK;
    25. }
  • 不带头结点

    • 对于头指针的方法,需要判断是否为第一个元素。

      • 因为插入元素需要保存上一个结点,不带头结点的链表,头指针无上一个结点,所以需要单独处理。 ```c / 08单链表——尾插法建立不带头节点/ Status tailInsert_SL(SL& sL, ElementType* datas, int length) { // 指针p,用来保存上一个结点 SL p = sL; // 指针s,用于创建新结点 SL s = NULL;

      for (int i = 0; i < length; i++) { // 指针p,每次循环在最后一个结点指针域,进行结点创建 s = (SL)malloc(sizeof(SL_Node)); if (!s) {

      1. return ERROR;

      } s->data = *(datas + i); s->next = NULL; // 不为第一个结点时 if (i) {

      1. // 将新节点连到p后面
      2. p->next = s;

      } // 为第一个结点时s新创建的地址赋值给链表标识sL,以及上一个结点标识p else {

      1. sL = s;

      } // 新节点作为上一个结点 p = s; // 指针q,再移动到新结点创建位置 s = s->next; } return OK; } ```

2 头插法

同样,这里是用于批量建立过程。

  • 下面两个方法是批量插入的方法,每一个插入的节点都是在头结点之后,即是head -> next,所以不需要额外的只针来保存该节点。如果是单个元素,则插入到指定位置前,需要从头找到前置节点。
  • 将该方法类比为单个元素的前插法。
    • 除了提供的插入元素指针s和插入位置的指针p,p向前找不能找到前置元素,需要从头找到p结点元素的前置元素。
    • 前插操作的两个语句可逆,因为三个结点都有指针进行保存。
  1. // 1. 找到p结点元素的前置结点
  2. // 头指针,指向头结点
  3. SL head;
  4. // 注意这里带头结点,从头结点开始;如果没有头结点,p为第一个元素的时候,需要特殊处理。
  5. SL q = head;
  6. while(q->next != p)
  7. {
  8. q = q->next;
  9. }
  10. // 2. 找到q后即开始前插操作。两个语句顺序可逆。
  11. s->next = p;
  12. q->next = s;
  • 带头结点

    1. /* 09_单链表——头插法建立_带头结点*/
    2. Status headInsert_SLh(SL& head, ElementType* datas, int length)
    3. {
    4. // 指针p,用作保存上一个创建的结点,使得头节点指向的新创建结点指向上一个结点。
    5. SL p = head;
    6. // 指针s,用于创建新的结点
    7. SL s = NULL;
    8. for (int i = 0; i < length; i++)
    9. {
    10. // 每次通过头结点创建新结点
    11. s = (SL)malloc(sizeof(SL_Node));
    12. if (!s)
    13. {
    14. return ERROR;
    15. }
    16. s->data = *(datas + i);
    17. // 元素接到头结点后
    18. head->next = s;
    19. // 第一个结点插入
    20. if (!i)
    21. {
    22. // 第一个结点即为表尾结点
    23. s->next = NULL;
    24. }
    25. // 非第一个结点
    26. else
    27. {
    28. // 结点前置为头结点
    29. head->next = s;
    30. // 结点后继为上一个结点
    31. s->next = p;
    32. }
    33. // 指针p保存当次插入的结点位置
    34. p = s;
    35. }
    36. return OK;
    37. }
  • 不带头节点

    1. /* 10_单链表——头插法建立_不带头结点*/
    2. Status headInsert_SL(SL& sL, ElementType* datas, int length)
    3. {
    4. // 同上,指针p用来保存上一个创建的结点
    5. SL p = sL;
    6. // 指针s,用来创建新的节点
    7. SL s = NULL;
    8. for (int i = 0; i < length; i++)
    9. {
    10. s = (SL)malloc(sizeof(SL_Node));
    11. if (!s)
    12. {
    13. return ERROR;
    14. }
    15. s->data = *(datas + i);
    16. // 第一个结点
    17. if (!i)
    18. {
    19. // 第一个结点作为表尾结点
    20. s->next = NULL;
    21. }
    22. else
    23. {
    24. // 连接到上一次插入的结点
    25. s->next = p;
    26. }
    27. // 连接到开始
    28. sL = s;
    29. // 保存为上一个结点
    30. p = s;
    31. }
    32. return OK;
    33. }

2.1.4 删除元素

删除指定值的元素并返回。每次必须通过前驱结点的next判断该结点,上前驱结点才好更改指针指向。

  • 带头节点 -> 带头结点的好处是,不用额外考虑元素个数。

    • 多一个头结点就刚好满足了每一个待判断的结点都有前驱,首元结点的前驱即是头结点。
    • 若链表没有元素,则不用进入循环体,没有额外的考虑。
      1. /* 14_单链表——删除结点_带头结点*/
      2. void deleteElem_SLh(SL& head, ElementType data)
      3. {
      4. // 保存待删除结点的上一个结点
      5. SL p = head;
      6. // 保存待删除结点
      7. SL q = NULL;
      8. while (p->next)
      9. {
      10. // 指针p总是在上一个结点的时候判断出下一个结点的条件
      11. if (p->next->data == data)
      12. {
      13. q = p->next;
      14. // 指针q若无后继结点,不影响
      15. p->next = q->next;
      16. free(q);
      17. return;
      18. }
      19. // 否则继续查找
      20. p = p->next;
      21. }
      22. }
  • 不带头节点

    • 不太方便。
      • 第一个元素没有前驱结点,需单独判断。
      • 但是否有第一个元素还需要判断。带头结点的删除则可以直接进入循环体中判断。
        1. /* 15_单链表——删除结点_不带头结点*/
        2. void deleteElem_SL(SL& sL, ElementType data)
        3. {
        4. // 指针p,保存待删除结点的前一个结点
        5. SL p = sL;
        6. // 指针q,指向p的后继结点
        7. SL q = p->next;
        8. // 如果链表没有结点,直接返回
        9. if (!p)
        10. {
        11. return;
        12. }
        13. // 如果第一个结点就是待删除结点
        14. else if (p && p->data == data)
        15. {
        16. sL = q;
        17. free(p);
        18. return;
        19. }
        20. // 两个以上结点
        21. while (p->next)
        22. {
        23. // 指针p总是在上一个结点的时候判断出下一个结点的条件
        24. if (p->next->data == data)
        25. {
        26. q = p->next;
        27. p->next = q->next;
        28. free(q);
        29. return;
        30. }
        31. p = p->next;
        32. }
        33. }

2.1.5 原地逆置单链表

『单链表原地逆置』个人理解。

  1. 大思路是将所有结点分为两种状态,『已完成逆置』和『待逆置』;
  2. 刚开始的状态,所有结点都是待逆置状态。
  3. 除了首元结点外,后续结点在执行逆置过程中都有结点处于已完成逆置状态。
  4. 为了方便理解,且不将『首元结点的逆置』与『其余结点逆置』过程作为不同子过程单独处理,需在首元结点逆置过程中『虚拟出一个空结点』作为『已完成逆置状态』的结点。
  5. 此次虚拟空结点作为『已完成逆置状态结点』,更方便将循环体划分出来。
  6. 难点在于划分出单轮次的循环过程。
  • 带头结点

    1. /* 16_单链表——原地逆置_带头节点*/
    2. void invertList_SLh(SL& head)
    3. {
    4. // 指针p保存下一个带改变指向的元素,保证断开后头结点的指针能找到
    5. SL p = head->next;
    6. // 用指针p保存了,此处一定要置空,作为虚拟出的空结点
    7. head->next = NULL;
    8. // 指针s是要改变指向的元素
    9. SL s = NULL;
    10. // 特别注意理解第一轮的过程,虚拟一个NULL结点在头结点和首元结点之间,作为【已完成逆置】的元素。
    11. while (p)
    12. {
    13. // 当前【待逆置的元素】完成,设置【下一个待逆置元素】为【当前待逆置】;
    14. s = p;
    15. // 指针p作为【下一个待逆置】
    16. p = p->next;
    17. // 当前待逆置元素,指向上一个逆置完成的元素(第一个结点的上一个元素即是虚拟出的空结点,此处在进入循环前置空)
    18. s->next = head->next;
    19. // 当前元素逆置完成,【头结点指针指向的元素】则是【上一轮逆置完成的元素】
    20. head->next = s;
    21. }
    22. }
  • 不带头结点

    • 实现上与带头结点的没有不同,仅仅头指针与头结点next域的替换。
      1. /* 17_单链表——原地逆置_不带头节点*/
      2. /* 过程同上,原理无不同,只有head->next替换为sL头指针*/
      3. void invertList_SL(SL& sL)
      4. {
      5. SL p = sL;
      6. sL = NULL;
      7. SL s = NULL;
      8. while (p)
      9. {
      10. s = p;
      11. p = p->next;
      12. s->next = sL;
      13. sL = s;
      14. }
      15. }

2.2 循环链表

  • 与单链表唯一的区别是,表尾的结点指向不是NULL。
    • 带头结点
      • 表为结点指向头结点
    • 不带头结点
      • 表尾结点指向首元结点
  • 基于这样一种变化,对于查询、遍历等循环结束条件,与链表是否为空的判断条件,由头结点next域或头指针是否为NULL,改成是否为头结点或首元结点

2.3 双向循环链表

  • 相对于单链表
    • 优势是,对于头插法的操作,不需要从表头开始进行遍历查找,可以直接找到前驱。
    • 劣势是,多维护一个前驱的关系,在某些情况可能比单链表耗性能。
  • 存储结构定义
    • 基于单链表结构的优化,反映链式存储关系;
    • 数据域定义与单链表无不同;
    • 指针域的定义,除了有指向后继结点元素的只针,还有指向前驱结点元素的只针; ```c // 定义返回状态及返回码

      define Status int

      define OK 1

      define ERROR 0

      // 定义存储的元素类型

      define ElementType int

/ 双向链循环表存储结点/ typedef struct DuL_Node { // 指向前驱节点指针 DuL_Node prior; // 存储元素 ElementType data; // 指向后继节点指针 DuL_Node next; }DuNode, *DuL;

  1. > <a name="84881111"></a>
  2. ### 常用操作
  3. <a name="cf9de519"></a>
  4. #### 2.3.1 初始化操作
  5. - 前驱与后继指针都指向头结点
  6. ```c
  7. /* 01_双向循环链表——初始化_带头结点*/
  8. Status initList_DuL(DuL& duL)
  9. {
  10. duL = (DuL)malloc(sizeof(DuL_Node));
  11. if (!duL)
  12. {
  13. return ERROR;
  14. }
  15. duL->data = NULL;
  16. // 循环链表,初始前置和后继时候指向自己
  17. duL->prior = duL;
  18. duL->next = duL;
  19. return OK;
  20. }

2.3.2 指定索引位置前插入元素

  • 不需要通过前驱结点来判断索引位置结点,找到后就直接对索引位置进行操作。

    1. /* 02_双向循环链表——在指定索引位置前插入元素*/
    2. Status insertByIndex_DuL(DuL& duL, int index, ElementType data)
    3. {
    4. // 定义:插入之前的元素结点
    5. DuL beforeInsert = duL;
    6. // 先找到待插入为值的前置结点
    7. for (int i=0;i<index;i++)
    8. {
    9. // 如果索引大于链表元素个数,则直接放最后
    10. if (beforeInsert->next != duL)
    11. {
    12. beforeInsert = beforeInsert->next;
    13. }
    14. }
    15. // 插入后的后继节点
    16. DuL afterInsert = beforeInsert->next;
    17. // 建立插入结点
    18. DuL curInsert = (DuL)malloc(sizeof(DuL_Node));
    19. if (!curInsert)
    20. {
    21. return ERROR;
    22. }
    23. curInsert->data = data;
    24. // 与前置结点关联
    25. beforeInsert->next = curInsert;
    26. curInsert->prior = beforeInsert;
    27. // 与后继结点关联
    28. curInsert->next = afterInsert;
    29. afterInsert->prior = curInsert;
    30. return OK;
    31. }

2.3.3 遍历

访问方法简单定义

  1. /* 05_双向循环链表——访问*/
  2. void visit(DuNode duNode)
  3. {
  4. printf("duNode data -> %d\n", duNode.data);
  5. }

1. 正向遍历
  • 通过next后继指针遍历
    1. /* 03_双向循环链表——正序遍历*/
    2. void traverseList_DuL(DuL duL)
    3. {
    4. DuL p = duL->next;
    5. while (p != duL)
    6. {
    7. visit(*p);
    8. p = p->next;
    9. }
    10. }

2. 反向遍历
  • 通过prior前驱指针遍历
    1. /* 04_双向循环链表——反向遍历*/
    2. void backTraverseList_DuL(DuL duL)
    3. {
    4. DuL p = duL->prior;
    5. while (p != duL)
    6. {
    7. visit(*p);
    8. p = p->prior;
    9. }
    10. }

2.3.4 批量插入元素到表尾

  • 访问正向或反向的表尾元素很简单,直接通过头节点的prior即可。
  • 相对于单链表而言,不需要从表头开始遍历到表尾。
    1. /* 06_双向循环链表——批量插入数据到表尾*/
    2. Status batchInsertToTail_DuL(DuL& duL, ElementType* datas, int length)
    3. {
    4. // 待插入的位置
    5. DuL tail = duL->prior;
    6. // 待插入的结点迭代
    7. DuL cur = NULL;
    8. for (int i = 0; i < length; i++)
    9. {
    10. // 每次创建一个结点
    11. cur = (DuL)malloc(sizeof(DuNode));
    12. // 插入失败则返回错误
    13. if (!cur)
    14. {
    15. return ERROR;
    16. }
    17. cur->data = *(datas + i);
    18. // 连接前置结点
    19. tail->next = cur;
    20. cur->prior = tail;
    21. // 连接后继结点
    22. cur->next = duL;
    23. duL->prior = cur;
    24. // 下一个
    25. tail = cur;
    26. }
    27. return OK;
    28. }

2.3.5 删除指定索引的元素

  • 不需要通过前驱结点访问到当前结点,因为双向的优势可以直接找前驱或者后继

    1. /* 07_双向循环链表——删除指定索引的元素*/
    2. ElementType deleteByIndex_DuL(DuL& duL, int index)
    3. {
    4. ElementType data;
    5. DuL cur = duL->next;
    6. // 直接找到该结点,不用找前置
    7. for (int i = 0; i < index; i++)
    8. {
    9. if (cur->next == duL)
    10. {
    11. return NULL;
    12. }
    13. cur = cur->next;
    14. }
    15. // 保存返回值
    16. data = cur->data;
    17. // 将前置节点和后继结点连接起来
    18. cur->prior->next = cur->next;
    19. cur->next->prior = cur->prior;
    20. // 释放空间
    21. free(cur);
    22. return data;
    23. }
  • 暂时先补充到这里,如果有常见的操作会更新加到这里。