2.3.1 单链表的操作与实现(数据结构-纯c语言)

2.3.1 单链表

每个节点除了存放数据元素外,还要存储指向下一个节点的指针

优点:不要求大片连续空间,改变容量方便

缺点:不可随机存取,要耗费一定空间存放指针

带头结点

操作详解

image-20231130233644701

  • 定义创建一个单链表

    typedef struct LNode {
          
                //定义单链表结点类型
        int data;               //每个节点存放一个数据元素
        struct LNode* next;     //指针指向下一个节点 指向结构体类型为LNode的指针变量next
    }LNode, *LinkList;
    
    //struct LNode* next这意味着每个Node结构体的next成员可以指向另一个Node结构体,从而形成链表或其他递归结构。
    
    
    int main(){
          
          
        
     	LinkList L;
        
        return 0;
    }
    
    • 关于LNode和*LinkList

      //上述代码相当于
      struct LNode
      {
              
              
          int data;
          struct LNode* next;
      };
      typedef struct LNode LNode;
      typedef struct LNode* LinkList;   //这是一个指向struct LNode类型的指针
      
      //注意LNode* LinkList 这样写更严谨一些;将LNode* 重新命名为LinkList
      
      
      /* 创建单链表的头指针L(L就是一个),然后指向头结点(俗称第0个结点)。第0个结点不保存数据,但是头结点的next指针域指向下一个节点。然后一个个链接起来
      */
      
    • 问题一:为什么要将LNode* 重新命名为LinkList?

      • LNode* 等价与 LinkList
      • LinkList 强调这是一个单链表或者头结点;LinkList L;
        LNode* 强调这是一个结点; LNode *p
    • 问题二:为什么创建单链表使用LinkList L,而不使用LNode L

      • 在创建单链表时,使用LinkList L 是为了方便对链表进行操作;LinkList是指向结点的指针类型的别名,而不是具体的结点类型。

      • 如果使用LNode L 来表示链表,L就是具体的结点类型的变量,而不是指向结点的指针。

        使用LinkList L,可以通过修改L的值来改变链表的头指针,方便操作整个链表。

    • 内存状态

      image-20231006150236031

  • 初始化单链表

    // 初始化一个单链表
    bool InitList(LinkList *L) {
          
          		
        *L = (LNode*)malloc(sizeof(LNode));     
        /*    L是指向指针的指针,表示传入的参数是一个指向链表头指针的指针
        当分配内存失败时,L的值为NULL,表示没有足够的内存来创建链表头结点*/
        if (*L == NULL)  // 内存不足,分配失败
            return false;
        (*L)->next = NULL;  // 头结点下一个指向NULL 防止脏数据。每次初始化,都设置,养成好习惯
        return true;
    }
    
    //对于这个地方bool InitList(LinkList* L),这样写bool InitList(LinkList *L)要好一些,便于理解
    //相当于LNode* *L,所以可以直接用(*L)->data,来指向结构体里面内容,注意.和->的用法。复杂写法(**L).data
    
    
    • 问题一:如何理解*L = (LNode*)malloc(sizeof(LNode));

      • 相当于是先申请了一个头结点(第0个结点)

      • 头指针L 指向第0个结点

        image-20231130235925945

      //等同于
      bool InitList(LinkList* L) {
              
              
          LNode* p = (LNode*)malloc(sizeof(LNode));   //申请的头结点为p
          if (L == NULL)  // 内存不足,分配失败
              return false;
          *L = p;                                     //头指针指向头结点p
          (*L)->next = NULL;  // 将结点中的next指针域设置为NULL
          return true;
      }
      
    • 问题二:为什么传入参数是LinkList *L

      • 在c语言中没用引用,main函数中初始化单链表得传入头指针L的地址LinkList(&L)
  • 按位插入

    插入操作,在表L中的第i个位置上插入指定元素e

    image-20231201003048316

    //按位序插入
    
    /*在第i个位置插入元素e
        1.循环找到i的前一个结点,此时p为目标位置的前一个结点
        2.申请一个节点s,依次修改指针
    */
    
    bool ListInsert(LinkList *L, int i, int e){
          
          
        if(i<1)
            return false;
        LNode* p;       //指针p指向当前扫描的结点
        int j = 0;      //当前p指向的是第几个结点
        p = *L;         //L指向头结点,头结点是第0个结点;p指向第0个结点
    //   	LNode* p = *L;   熟练了可以直接这么写,让p直接指向头结点
        while(p!=NULL&& j<i-1){
          
          
            p = p->next;
            j++;
        }
        if(p==NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        s->data = e;
        s->next = p->next;	//s的next指针域   指向  p的next指针域所指向的结点
        p->next = s;
        return true;
    }
    
    
    • 问题一:为什么要找到前一个结点

      为了修改前一个结点的next指针指向位置,让他能够指向我插入的地方

    • 问题二:修改步骤1和2反了会出现什么

      由于p的next指针域先指向了s,导致s->next = p->next; s指向了自己

      image-20231201004104521

  • 指定结点后插操作

    在结点p之后,插入新的元素e(实际上将按位插入给拆开了)

    // 后插操作:在p结点之后插入元素e
    bool InsertNextNode(LNode* p, int e) {
          
          
        if (p == NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        if (s == NULL)
            return false;
        s->next = p->next;
        p->next = s;
        s->data = e;
        return true;
    }
    
    
    bool ListInsert(LinkList* L, int i, int e) {
          
          
        if (i < 1)
            return false;
        //首先还是先找到结点p
        LNode* p;   
        int j = 0; 
        p = *L;     
        while (p != NULL && j < i - 1) {
          
          
            p = p->next;
            j++;
        }
        return InsertNextNode(p, e);
    }
    
    
    
    • 问题一:为什么传参使用LNode *p 不用LNode **p

      这是因为链表的插入操作需要修改指针的指向,在第一种写法中,通过§->next来访问p指针所指向的结点的next指针域,p指针本身并没有被修改;而在第二种写法中,通过传递指针p的地址来修改p指针所指向的结点的next指针域

    • 问题二:为什么 要写判断这个 if (p == NULL) return false;

      如果传入的的p指向空链表L,L为空,p也为空,无法插入

  • 指点结点前插操作

    在结点p之前插入 元素e

    采用方位为后插,然后交换数据

    image-20231201162643917

    //指定结点前插操作
    //由于是在p结点之前插入,找不到前驱结点(当然也可以传入单链表来找前驱结点,那么时间复杂度明显较高)
    //采用方式为,正常的后插操作,然后将p和s的元素进行互换,即完成前插操作
    
    bool InsertPriorNode(LNode* p,int e){
          
          
        if(p==NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        if(s==NULL)
            return false;
        s->next = p->next;
        p->next = s;
        s->data = p->data;
        p->data = e;
        return true;
    }
    
  • 按位删除

    删除第i个位置的结点,并将删除的数据返回到e中

    image-20231201170500692

    //按位删除
    bool ListDelete(LinkList *L,int i,int *e){
          
          
        if(i<1)
            return false;
        //还是需要找到前一个结点
        LNode* p = *L;
        int j = 0;
        while(p!=NULL&&j<i-1){
          
          
            p = p->next;
            j++;
        }
        if(p==NULL)
            return false;
        if(p->next = NULL)
            return false;   //如果p后面一个结点为NULL,那么就删除失败
        LNode* q = p->next; //q指向要被删除的结点
        *e = q->data;
        p->next = q->next;
        free(q);
        return true;
    }
    //最坏、平均时间复杂度:O(n)
    //最好时间复杂度:O(1)
    
  • 指定结点的删除操作

    给出结点p,删除结点中的数据

    由于给出结点,没有前驱结点采用方式为:将p变为前驱结点,将后面的数据复制到p中,p的next指针域指向q的next指针域指向的结点,释放q

    简单来说,就是交换数据,然后删除后面结点

    image-20231202144521563

    //指定结点删除
    bool DeleteNode(LNode *p){
          
          
        if(p==NULL||p->next==NULL)
            return false;
        
        LNode* q = p->next;     //令q指向p的后继结点
        p->data = q->data;    //将q结点中的数据给p
        p->next = q->next;          //将q结点从链中断开
        free(q);                //释放q
        return true;
    }
    
    //此处删除操作仍存在小问题,对于最后一个结点,采用的方式是直接返回false,也就是说,未能实现删除最后一个结点,如果需要准确删除,需要传入单链表的头结点来一次遍历进行删除,时间复杂度也会相应提高
    
  • 按位查找

    返回第i个位置的结点p

    // 单链表按位查找
    LNode* GetNode(LinkList* L, int i) {
          
          
        if (i < 0)
            return NULL;
        LNode* p = *L;
        int j = 0;
        while (p != NULL && j < i) {
          
          
            p = p->next;
            j++;
        }
        return p;
    }
    
    • 问题一:为什么是i<0,不是i<1
      • 这是代头结点的单链表,i=0,可以认为是头结点;
      • 之前写的按位插入,开头都是要找到第i个结点的前一个结点,那么就可以直接调用这个方法 LNode *p = GetNode(*L, i-1);当插入的结点在第1个位置时,此时i-1为0 就体现出来了
  • 按值查找

    //按值查找,找到数据域==e 的结点
    LNode *LocationElem(LinkList L,int e){
          
          
        LNode* p = L->next;
        //从第一个节点开始查找数据域为e的结点
        while(p!=NULL&&p->data!=e)
            p = p->next;
        return p;       //找到后返回该结点指针,否则返回NULL
    }
    
  • 求表长度

    //求表长度
    int Length(LinkList L){
          
          
        int len = 0;
        LNode* p = L;
        while(p!=NULL){
          
          
            p = p->next;
            len++;
        }
        return len;
    }
    
    • 问题一:为什么是LNode* p = L;,不是LNode* p = L->next;从第一个结点开始算起

      在下面的while循环中,先让p结点移动一次,然后再让len++,所以从第0个结点开始

  • 建立单链表

    尾插法:与按位插入类似,在每个节点后面插入新的节点

    image-20231202203249765

    // 用尾插法建立单链表
    LinkList List_tailInsert(LinkList* L) {
          
          
        int x;                               // 设element为整形
        *L = (LNode*)malloc(sizeof(LNode));  // 建立头结点
        LNode *s, *r = *L;
        scanf("%d", &x);
        while (x != -1) {
          
          
            s = (LNode*)malloc(sizeof(LNode));
            s->data = x;  // 申请的新结点s存储数据x
            r->next = s;  // r的next指针域指向s
            r = s;        // r指向s,相当于移到s这边
            scanf("%d", &x);
        }
        r->next = NULL;     //尾结点指针置空
        return *L;           // 注意:返回的是*L
    }
    

    头插法:每次在链表的头部进行插入操作建立单链表

    image-20231203002256870

    // 用头插法建立单链表
    LinkList List_HeadInsert(LinkList *L){
          
          
        LNode* s;
        int x;
        *L = (LNode*)malloc(sizeof(LNode));
        if (*L == NULL)
            return NULL;
        (*L)->next = NULL;      //头插法建立,这里需要将L的next指针域指向NULL 
    
        scanf("%d", &x);
        while(x!=-1){
          
          
            s = (LNode*)malloc(sizeof(LNode));
            if(s==NULL)
                break;	//申请内存失败
            s->data = x;
            s->next = (*L)->next;
            (*L)->next = s;
            scanf("%d", &x);
        }
        return *L;
    }
    
    

完整代码

// 带头结点的单链表
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>


typedef struct LNode {
    
    
    int data;
    struct LNode* next;
} LNode, *LinkList;

// 初始化一个单链表
bool InitList(LinkList *L) {
    
    
    *L = (LNode*)malloc(sizeof(LNode));
    /* L是指向指针的指针,表示传入的参数是一个指向链表头指针的指针
    当分配内存失败时,L的值为NULL,表示没有足够的内存来创建链表头结点*/
    if (*L == NULL)  // 内存不足,分配失败
        return false;
    (*L)->next = NULL;  // 头结点之后暂时还没有结点
    return true;
}

bool Empty(LinkList* L) {
    
    
    return (*L)->next == NULL;
}

// 后插操作:在p结点之后插入元素e
bool InsertNextNode(LNode* p, int e) {
    
    
    if (p == NULL)
        return false;
    LNode* s = (LNode*)malloc(sizeof(LNode));
    if (s == NULL)
        return false;
    s->next = p->next;
    p->next = s;
    s->data = e;
    return true;
}

/*在第i个位置插入元素e(带头结点)
    1.循环找到i的前一个结点,p指向他的next指针域,p=p->next
    2.申请一个节点s,依次修改指针
*/
// 按位序插入(带头结点)
bool ListInsert(LinkList* L, int i, int e) {
    
    
    if (i < 1)
        return false;
    LNode* p;   // 指针p指向当前扫描的结点
    int j = 0;  // 当前p指向的是第几个结点
    p = *L;     // L指向头结点,头结点是第0个结点;
    while (p != NULL && j < i - 1) {
    
    
        p = p->next;
        j++;
    }
    /*     if(p==NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        s->data = e;
        s->next = p->next;
        p->next = s;
        return true; */
    return InsertNextNode(p, e);
}

// 前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode* p, int e) {
    
    
    if (p == NULL)
        return false;
    LNode* s = (LNode*)malloc(sizeof(LNode));
    if (s == NULL)
        return false;  // 内存分配失败
    s->next = p->next;
    p->next = s;        // 将新结点s连到p之后
    s->data = p->data;  // 将p中元素复制到s中
    p->data = e;        // p中元素覆盖为e
    return true;
}

// 指定结点删除
// 因为没有前一个结点,采用方式:申明指针q指向p的后继结点,然后夺舍p,释放q
bool DeleteNode(LNode* p) {
    
    
    if (p == NULL||p->next==NULL)
        return false;
    LNode* q = p->next;       // 令q指向*p的后继结点
    p->data = p->next->data;  // 将后继结点夺舍*p结点
    p->next = q->next;        // 将*q结点从链中断开
    free(q);                  // 释放q
    return true;
}

// 按位序删除(带头结点)
bool ListDelete(LinkList* L, int i, int* e) {
    
    
    if (i < 1)
        return false;
    LNode* p;                         // 当前p指向当前扫描的结点
    int j = 0;                        // 当前p指向第几个结点
    p = *L;                           // p指向第0个结点
    while (p != NULL && j < i - 1) {
    
      // 循环找到第i-1个结点
        p = p->next;
        j++;
    }
    if (p == NULL)
        return false;  // i值不合法(超了)
    if (p->next == NULL)
        return false;
    LNode* q = p->next;  // q指向要被删除的结点,即p的下一个结点
    *e = q->data;        // 返回e的数值
    p->next = q->next;   // q指向的结点直接断开
    free(q);
    return true;
}

// 按位查找,返回第i个结点,带头结点
LNode* GetElem(LinkList L, int i) {
    
    
    if (i < 0)
        return NULL;
    LNode* p;
    int j = 0;
    p = L;
    while (p != NULL && j < i) {
    
      // 循环找到第i个结点
        p = p->next;
        j++;
    }
    return p;
}

// 按值查找,找到数据域==e 的结点
LNode* LocationElem(LinkList L, int e) {
    
    
    LNode* p = L->next;
    // 从第一个节点开始查找数据域为e的结点
    while (p != NULL && p->data != e)
        p = p->next;
    return p;  // 找到后返回该结点指针,否则返回NULL
}

// 求表的长度
int Length(LinkList L) {
    
    
    int len = 0;
    LNode* p = L;
    while (p != NULL) {
    
    
        p = p->next;
        len++;
    }
    return len;
}

// 用尾插法建立单链表
LinkList List_tailInsert(LinkList* L) {
    
    
    int x;                               // 设element为整形
    *L = (LNode*)malloc(sizeof(LNode));  // 建立头结点
    LNode *s, *r = *L;
    scanf("%d", &x);
    while (x != -1) {
    
    
        s = (LNode*)malloc(sizeof(LNode));
        s->data = x;  // 申请的新结点s存储数据x
        r->next = s;  // r的next指针域指向s
        r = s;        // r指向s,相当于移到s这边
        scanf("%d", &x);
    }
    r->next = NULL;     //尾结点指针置空
    return *L;           // 注意:返回的是*L
}

// 用头插法建立单链表
LinkList List_HeadInsert(LinkList *L){
    
    
    LNode* s;
    int x;
    *L = (LNode*)malloc(sizeof(LNode));
    if (*L == NULL)
        return NULL;
    (*L)->next = NULL;      //头插法建立,这里需要将L的next指针域指向NULL 

    scanf("%d", &x);
    while(x!=-1){
    
    
        s = (LNode*)malloc(sizeof(LNode));
        if(s==NULL)
            break;
        s->data = x;
        s->next = (*L)->next;
        (*L)->next = s;
        scanf("%d", &x);
    }
    return *L;
}

//  输出单链表中的元素
void printList(LinkList L) {
    
    
    LNode* p = L->next;  // p此时指向第1个结点(跳过头结点)
    int j = 1;
    if (L == NULL)
        return;  // 如果链表为空,则直接返回

    while (p != NULL) {
    
      // 遍历单链表
        printf("第%d个节点为:%d\n", j, p->data);
        p = p->next;  // 移动到下一个节点
        j++;
    }
}

int main() {
    
    
    LinkList L;
    List_HeadInsert(&L);
    // List_tailInsert(&L);
    ListInsert(&L, 2, 1314);
    ListInsert(&L, 3, 520);
    ListInsert(&L, 4, 0);
    int e;
    ListDelete(&L, 4, &e);
    printf("被删除数据为%d\n", e);
    printList(L);

    system("pause");
}

不带头结点

  • 创建与初始化

    typedef struct LNode
    {
          
          
        int data;
        struct LNode* next;
    }LNode,*LinkList;
    
    //初始化
    bool InitList(LinkList *L){
          
          
        *L = NULL;
        return true;
    }
    
  • 按位插入

    image-20231203003830808

    对于不带头结点,由于没有头结点,第一个插入需要单独处理

    //按位插入
    bool ListInsert(LinkList *L,int i,int e){
          
          
        if(i<1)
            return false;
        if(i==1){
          
          
            LNode* s = (LNode*)malloc(sizeof(LNode));
            if(s==NULL)
                return false;
            s->data = e;
            s->next = *L;
            *L = s;
            return true;
        }
        LNode* p = *L;  // p指向当前扫描的结点
        int j = 1;      // 此时指向第一个节点
        while(p!=NULL&&j<i-1){
          
          
            p = p->next;
            j++;
        }
        if(p==NULL)
            return false;
        LNode* s = (LNode*)malloc(sizeof(LNode));
        s->data = e;
        s->next = p->next;
        p->next = s;
        return true;
    }
    
    
    • 问题一:为什么需要单独处理第一个插入位置

      从方法看,插入是需要用p来找到前一个结点,由于第一个位置前面没有结点,所以需要修改头指针的指向来进行插入;而带头结点的单链表头指针始终指向于头结点,所以p可以直接找到

  • 按位删除

    根据位置,删除结点,同样需要对第一个节点进行单独处理

    // 按位删除
    bool ListDelete(LinkList* L, int i, int* e) {
          
          
        if (i < 1)
            return false;
        if (i == 1) {
          
          
            LNode* q = *L;    //q指向头指针所指向的结点,即第一个结点
            *L = q->next;      //头指针指向 q的next指针域所指向的结点,如果没有第二个结点则为NULL 
            *e = q->data;
            free(q);
            return true;
        }
        // 与之前删除相同
        LNode* p = *L;
        int j = 1;
        while (p != NULL && j < i - 1) {
          
          
            p = p->next;
            j++;
        }
        // 检查第 i-1 个结点以及第 i 个节点是否存在
        if (p == NULL || p->next == NULL)
            return false;
        LNode* q = p->next;
        p->next = q->next;
        *e = q->data;
        free(q);
        return true;
    }
    

    具体操作会在栈和队列中,不带头结点用的较多

猜你喜欢

转载自blog.csdn.net/weixin_46290752/article/details/134760292
今日推荐