跳过正文
  1. 嵌入式学习笔记/
  2. 数据结构/

链表

目录
数据结构 - 这篇文章属于一个选集。
§ 3: 本文

链表
#

**链表(linked list)**是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接.引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点. 这样就可以让各个节点分别存储在不同地址,内存无需连续

链表的组成单位是节点,每个节点包含两个数据:

  • data:数据,就是节点的值
  • next指针:指向下一个节点的指针

这也就是说链表比数组占用更多的内存空间

/* 链表节点结构体 */
typedef struct ListNode {
    int val;               // 节点值
    struct ListNode *next; // 指向下一节点的指针
} ListNode;

/* 构造函数 */
ListNode *newListNode(int val) {
    ListNode *node;
    node = (ListNode *) malloc(sizeof(ListNode));
    node->val = val;
    node->next = NULL;
    return node;
}

头节点
#

单链表开始节点前可以设立一个节点成为头节点,它的数据域可以不存储任何信息也可以存储链表长度等信息,头结点的指针域指向第一个节点(首元节点)的指针

头指针
#

链表中指向第一个节点的指针,有头节点时指向头结点,无头节点指向首元节点

单链表(带头节点)
#

链表初始化
#

建立链表分为两步,第一步是初始化各个节点对象,第二步是构建节点之间的引用关系.初始化完成后,我们就可以从链表的头节点出发,通过引用指向next依次访问所有节点.

/* 先定义一个带头节点的单链表 */
typedef struct LNode{           //定义单链表节点类型
    ElemType data;              //每个节点存放一个数据元素
    struct LNode *next;         //指针指向下一个节点
}LNode, *LinkList;              //LNode 是节点类型,LinkList是指向LNode这种节点的指针类型


// 初始化一个单链表(带头结点)
bool InitList(LinkList &L){
    L = (LNode * ) malloc(sizeof(LNode));   //分配一个头节点
    if(L == NULL)                           //  内存不足,分配失败
        return false;
    L->next = NULL;                         //头节点之后暂时还没有节点
    return true;
}

链表插入节点(按位序插入)
#

链表插入节点非常容易,只需要改变插入节点前节点的指针即可,时间复杂度为O(1) 相比数组插入元素的时间复杂度O(n),效率高多了

//在第 i 个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
    if(i < 1)
        return false;
    LNode *p;                       //指针p指向当前扫描到的节点
    int j = 0;                      //当前p指向的是第几个节点
    p = L;                          //L指向头节点,头节点是第0个节点(不存数据)

    //寻找第 i-1 个节点
    while(p != NULL && j < i-1){    //循环找到第 i-1 个节点
        p = p->next;
        j++;
    }

    if(p == NULL)                   //i值不合法
        return false;

    //新建一个节点并赋值要插入的数据e
    LNode *s = (LNode *)malloc(sizeof(LNode));
    s->data = e;

    //开始插入
    s->next = p->next;              //先将新节点的next指向第 i 个节点
    p->next = s;                    //将节点s连接到p之后

    return true;                    //插入成功
}

链表删除节点
#

与插入一样,删除也只需要改变一个节点的指针

// 按位序删除,i=1删表头,i=length删头尾
bool List_Del (LinkList &L, int i, ElemType &e) {           //删除时返回删除的元素
    if (i < 1 || i > List_Length(pHead)) return false;      //删除位置不合法
    LNode *p;                                               //指针p指向当前扫描到的节点
    int j = 0;                                              //当前p指向的第几个节点
    p = L;                                                  //L和p相同,都指向头节点

    //寻找第 i-1 个节点
    while(p != NULL && j < i-1){                            //循环找到第 i-1 个节点
        p = p->next;
        j++;
    }

    LNode *q = p->next;                                     // q指向待删除结点
    e = q->data;                                            //返回即将删除的数据
    p->next = q->next;                                      // 将q节点从链中“断开”
    free(q);                                                //释放节点的存储空间
    return true;                                            //删除成功
}

访问节点
#

链表的访问效率很低,因为访问需要从头节点一个一个的寻找next节点,时间复杂度为O(n)

/* 访问链表中索引为 index 的节点 */
ListNode *access(ListNode *head, int index) {
    for (int i = 0; i < index; i++) {
        if (head == NULL)
        return NULL;
        head = head->next;
    }
    return head;
}

查找节点
#

/* 在链表中查找值为 target 的首个节点 */
int find(ListNode *head, int target) {
    int index = 0;  
    while (head) {
        if (head->val == target)
            return index;
        head = head->next;
        index++;
    }
    return -1;
}

建立单链表
#

建立单链表常用的方法有头插法和尾插法

尾插法
#

新节点总是插入在链表的尾部(最后一个位置)。

尾插法的特点是每插入一个新节点,链表的尾节点指针(pTail)会更新为新插入的节点,使其始终指向当前链表的尾结点。从而使得输入的数据在链表中按顺序存储>。 当输入数据为 999 时,循环结束,将尾节点的 next 指针置为 NULL 表示链表结束,函数返回最终的链表头节点。

// 创建链表-通过尾插法
LinkList List_Create_Tail_Insert(LinkList &pHead) {
LNode *pTemp; // 临时节点指针
LNode *pTail = pHead; // 尾节点指针,初始指向头结点
int x; // 用于存储输入的数据
scanf("%d", &x);
while (x != 999) {
   pTemp = (LNode *)malloc(sizeof(LNode)); // 分配内存并创建新节点
   pTemp->data = x; // 尾节点的下一个指针指向新节点
   pTail->next = pTemp; // 更新尾节点指针,使其指向新节点
   pTail = pTemp; // 读取下一个数据
   scanf("%d", &x);
}
pTail->next = NULL; // 尾节点的下一个指针置为 NULL,表示链表结束
return pHead; // 返回带头结点的链表头指针
}

头插法
#

头插法的结果为倒序

该代码通过头插法创建一个链表. 头插法的特点是每插入一个新节点,链表的头节点就会变成新插入的节点,从而使得输入的数据在链表中是倒序存储>的. 当输入数据为 999 时,创建链表的循环结束,函数返回最终的链表头节点.

// 创建链表,头插法结果为倒叙
LinkList List_Create(LinkList &pHead) {
LNode *pTemp; int x; // 临时节点指针
scanf("%d", &x);
while (x != 999) {
    pTemp = (LNode *)malloc(sizeof(LNode)); // 分配内存并创建新节点
    pTemp->data = x;
    pTemp->next = pHead->next; // 新节点的指针指向当前头节点的下一个节点
    pHead->next = pTemp; // 头指针指向新节点,使其成为新的头节点
    scanf("%d", &x);
}
return pHead; // 返回带头结点的链表头指针
}

双链表
#

单链表结点中只有一个指向其后继的指针,使得单链表只能从前往后依次遍历.要访问某个结点的前驱(插入、删除操作时),只能从头开始遍历,访问前驱的时间复杂度为 O(n). 为了克服单链表的这个缺点,引入了双链表,双链表结点中有两个指针prior和next,分别指向其直接前驱和直接后继.

双链表的主要特性
#

  • 双向遍历由于每个节点都有前后两个指针,因此可以在列表中双向遍历,无需像单链表那样只能从头节点开始向前遍历。
  • 插入与删除的便捷性:在双链表中插入或删除一个节点时,只需改变相应节点的前后节点的指针指向即可,操作相对简单高效。

数据结构
#

  • data:数据域,也是节点的值
  • prior:指针域,指向前一个结点的指针
  • next:指针域,指向下一个结点的指针
/* 双链表中查找值为 target 的首个节点 */
typedef struct DNode {
    int data; // 数据
    struct DNode *prior, *next; // 前驱和后继指针
} DNode, *DLinkList;

双链表在单链表结点中增加了一个指向其前驱的指针 prior ,因此双链表的按值查找和按位查找的操作与单链表的相同.但双链表在插入和删除操作的实现上,与单链表有着较大的不同. 这是因为“链”变化时也需要对指针 prior 做出修改,其关键是保证在修改的过程中不断链. 此外,双链表可以很方便地找到当前结点的前驱,因此,插入、除操作的时间复杂度仅为 O(1).

按位序插入节点
#

// 按位序插入,i=1插在表头,i=length+1插在表尾
bool DList_Insert(DLinkList &pHead, int i, int e) {
  if (i < 1 || i > DList_Length(pHead) + 1) 
    return false;
  int j = 0; // 当前p指向的是第几个结点
  DNode *p = pHead; // p 指向头结点
  while (p != NULL && j < i - 1) { // 循环找到要插入位置的前驱结点
  p = p->next;
  j++;
}

  DNode *pTemp = (DNode *)malloc(sizeof(DNode));
  pTemp->data = e;
  pTemp->next = p->next;
  pTemp->prior = p;
  // 又后续结点,不是空链表
  if (p->next != NULL) {
    p->next->prior = pTemp;
  }
  p->next = pTemp;
  return true;
}

按位序删除节点
#

// 按位序删除,i=1删表头,i=length删头尾
bool DList_Del (DLinkList &pHead, int i) {
  if (i < 1 || i > DList_Length(pHead)) 
    return false;
  DNode *p = pHead;

  // 找到待删除位序的前一位结点
  for (int j = 0; j < i - 1; j++)
    p = p->next;
  DNode *q = p->next; // 待删除结点
  p->next = q->next;
  if (q->next != NULL)
    q->next->prior = p;
  free(q);
  return true;
}```
RuChen
作者
RuChen
数据结构 - 这篇文章属于一个选集。
§ 3: 本文