Bootstrap

数据结构 | 第二章 | 链表介绍 | 单链表

一、引入

  • 动态顺序表

    • 插入数据,空间不够了,要增容

    • 要求数据是依次存储

  • 缺陷

    • 如果空间不够,增容。增容会付出一定性能消耗,其次可能存在一定的空间浪费
    • 如果100个空间,满了,然后增容到200(每次增容*2),但是我实际就插入了10个数据,如果不在插入,那么就有90个空间浪费了。
    • 头部或者中部左右的插入删除效率低,为O(n),每次头插都要将n个数据后移。
  • 如何解决?

    • 1、空间上,按需给空间
    • 2、不要求物理空间连续,头部和中间的插入就不需要挪动数据。
    • 3、引出—>链表

二、基本介绍

  1. 链表的概念和结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

image-20241019193039296

链表的物理结构:(实实在在的内存中是如何存储表示他们之间关系的)

image-20241019195331059

  • 首先需要一个头指针(图中为p)指向第一个元素的地址

三、代码实现

SList.h头文件

// Created by zjc on 2024/10/19 17:15

//#ifndef SLSIT_SLIST_H
//#define SLSIT_SLIST_H
//
//#endif //SLSIT_SLIST_H

#pragma once  // 防止头文件被多次包含

#include "stdio.h"
#include "stdlib.h"

// typedef定义类型,方便后面更改
typedef int SLTDataType;

struct SListNode {
    // 1.定义两个变量,一个是值(数据),一个是地址(下一个数据的地址)
    // 类型和存储的值
    SLTDataType data;
    // 指向下一个节点的指针(地址);类型是一样的也是结构体
    struct SListNode *next;
};

// 进行下命名,更方便
typedef struct SListNode SLTNode;

// 打印
// 注意:不会改变链表的头指针,传一级指针
void SListPrint(SLTNode *phead);

// 接口:
// 注意:可能会改变链表的头指针,传二级指针
// 1.尾插 传入结构体指针和插入的数据x
void SListPushBack(SLTNode **phead, SLTDataType x);

// 2.头插 传入结构体指针和插入的数据x
void SListPushFront(SLTNode **pphead, SLTDataType x);

// 3.头删
void SListPopFront(SLTNode **pphead);

// 4.尾删
void SListPopBack(SLTNode **pphead);

// 5.查找
SLTDataType* SListFind(SLTNode* phead,SLTDataType x);

// 6.随机插入
void SListInsert(SLTNode **pphead,SLTNode* pos,SLTDataType x);

// 7.随机删除
void SListErase(SLTNode **pphead,SLTNode* pos);

/*
 * 有些地方给的是int,直接给的下标
 // 6.随机插入
void SListInsert(SLTNode **pphead,int pos,SLTDataType x);

// 7.随机删除
void SListErase(SLTNode **pphead,int pos);
 */

接口实现SList.c:

#include "SList.h"

// 0.打印
void SListPrint(SLTNode *phead) {
    SLTNode *cur = phead;
    // 当前指针位置等于NULL结束
    while (cur != NULL) {
        printf("%d->", cur->data);
        // 然后到下一个位置,将下一个地址赋值给当前
        cur = cur->next;
    }
    printf("NULL");
    printf("\n");
}

// 0.空间开辟
SLTNode *BuySListNode(SLTDataType x) {
    // 分配空间:
    // 定义结构体指针(地址)并且分配空间
    SLTNode *newnode = (SLTNode *) malloc(sizeof(SLTNode));
    // 开辟空间插入的数据
    newnode->data = x;
    // 开辟空间的下一个指针为NULL
    newnode->next = NULL;

    // 将新开的空间返回
    return newnode;
}
  1. 尾插

image-20241021162835923

// 1. 尾插
void SListPushBack(SLTNode **pphead, SLTDataType x) {
    // 分配空间:
    SLTNode *newnode = BuySListNode(x);

    // 一开始phead是NULL,是无法取到地址的,所以要进行判断,如果为空,将开辟空间的地址赋值给phead
    if (*pphead == NULL) {
        *pphead = newnode;
    } else // 如果说有多个就去找尾
    {
        // 找到尾结点的指针;因为每个节点存储下一个节点的地址
        SLTNode *tail = *pphead;
        while (tail->next != NULL) {
            tail = tail->next;
        }
        // 尾结点指针指向下一个地址就是新开辟的地址
        // 尾节点链接新节点
        tail->next = newnode;
    }
}
  1. 头插

image-20241021162912494

// 2.头插
void SListPushFront(SLTNode **pphead, SLTDataType x) {
    // 分配空间: 初始化
    SLTNode *newnode = BuySListNode(x);

    // 将之前的头指针指向第一个元素的地址赋值给新开辟的空间的next
    newnode->next = *pphead;

    // 此时的phead需要存第一个节点的地址
    *pphead = newnode;
}
  1. 头删

image-20241021163004372

// 3.头删
void SListPopFront(SLTNode **pphead) {
    // 先把下一个指针保存起来,这里需要括起来,因为->和*优先级一样
    // **pphead解引用才是  SLTNode*
    SLTNode *next = (*pphead)->next;

    // 如果直接将当前位置free是找不到下一个
    free(*pphead);

    // 将开始的pphead的地址赋值给头指针
    *pphead = next;
}
  1. 尾删

image-20241021163057783

// 4.尾删
void SListPopBack(SLTNode **pphead) {
    /*
    注意:
     1、注意链表为空的情况
     2、链表只有一个的情况,那么第一个next都为空,循环就不会进入
     3、最后释放了,就会存在野指针
     */
    // 1.如果链表是空,就不进行删除
    if (*pphead == NULL) {
        return;
    } else if ((*pphead)->next == NULL) // 2.只有一个节点的处理
    {
        // 直接将这个节点free,将pphead置为NULL
        free(*pphead);
        *pphead = NULL;
    } else {   // 3.一个以上节点
        // 定义一个指针找到tail的前一个节点
        SLTNode *prev = NULL;
        SLTNode *tail = *pphead;
        while (tail->next != NULL) {
            // tail往下走的时候,将我自己的位置给prev
            prev = tail;
            tail = tail->next;
        }
        // free掉tail
        free(tail);
        // 将tail前一个节点的指向指为NULL
        prev->next = NULL;
    }
}
  1. 查找

image-20241021163121979

// 5.查找
SLTDataType *SListFind(SLTNode *phead, SLTDataType x) {
    // 将首地址赋值给cur
    SLTNode *cur = phead;
//    while(cur != NULL){ 这种写法也可以
    // cur不等于0则继续
    while (cur) {
        // 如果当前的数据是查找的则返回
        if (cur->data == x) {
            return cur;
        }
        // 查找不到就继续下一个节点
        cur = cur->next;
    }
    return NULL;
}


  1. 随机插入

image-20241021163154442

// 6.随机插入
// 在pos的前面插入x
void SListInsert(SLTNode **pphead, SLTNode *pos, SLTDataType x) {
    // 如果pos等于pphead,就相当于头插
    if (pos == *pphead) {
        SListPushFront(pphead, x);
    } else { // 如果不是头插就是下面的逻辑
        // 开辟空间
        SLTNode *newnode = BuySListNode(x);

        // 也可以这样,prev的下一个节点等于pos就相当于找到了
        SLTNode *prev = *pphead;

        // 循环到下一个节点,prev就是pos前面的节点
        while (prev->next != pos) {
            prev = prev->next;
        }

        // 插入空间的地址赋值给pos前一个的next
        prev->next = newnode;
        newnode->next = pos;
    }
}
  1. 随机删除

image-20241021163232257

// 7.随机删除
void SListErase(SLTNode **pphead, SLTNode *pos) {
    // 如果说pos等于第一个节点,就相当于头删
    if (pos == *pphead) {
        SListPopFront(pphead);
    } else {
        // 也可以这样,prev的下一个节点等于pos就相当于找到了
        SLTNode *prev = *pphead;

        // 循环到下一个节点,prev就是pos前面的节点
        while (prev->next != pos) {
            prev = prev->next;
        }
        // 删除的前一个节点需要指向删除的下一个节点
        prev->next = pos->next;
        free(pos);
    }
}

测试文件Test.c

// Created by zjc on 2024/10/19 19:11

#include "SList.h"

void TestSList1() {
    // 定义一个头指针为空plist/phead
    SLTNode *plist = NULL;
    // 尾插
    // 注意:这里需要传递地址,结构体指针指向的还是另一个地址
    SListPushBack(&plist, 1);
    SListPushBack(&plist, 2);
    SListPushBack(&plist, 3);
    SListPushBack(&plist, 4);
    SListPushFront(&plist, 4);
    // 头删
    SListPopFront(&plist);
    // 打印
    SListPrint(plist);
}

void TestSList2() {
    // 定义一个头指针为空plist/phead
    SLTNode *plist = NULL;
    // 尾插:
    // 注意:这里需要传递地址,结构体指针指向的还是另一个地址
    SListPushBack(&plist, 1);
    SListPushBack(&plist, 2);
    SListPushBack(&plist, 3);
    SListPushBack(&plist, 4);

    // 尾删:
    SListPopBack(&plist);
    SListPopBack(&plist);
    SListPopBack(&plist);


    SListPrint(plist);
}

void TestSList3() {
    // 定义一个头指针为空plist/phead
    SLTNode *plist = NULL;
    // 尾插:
    // 注意:这里需要传递地址,结构体指针指向的还是另一个地址
    SListPushBack(&plist, 1);
    SListPushBack(&plist, 2);
    SListPushBack(&plist, 3);
    SListPushBack(&plist, 4);

    SListPrint(plist);

    // 想要在3的前面插入一个30
    //SLTNode *pos = SListFind(plist, 3);

    // 想要在1的前面插入一个10
    SLTNode *pos = SListFind(plist, 1);

    // 如果说pos不为空,则将数据插入
    if (pos) {
        // 找到位置后,在作为参数传过来
        SListInsert(&plist, pos, 10);
    }
    SListPrint(plist);
}

void TestSList4() {
    // 定义一个头指针为空plist/phead
    SLTNode *plist = NULL;
    // 尾插:
    // 注意:这里需要传递地址,结构体指针指向的还是另一个地址
    SListPushBack(&plist, 1);
    SListPushBack(&plist, 2);
    SListPushBack(&plist, 3);
    SListPushBack(&plist, 4);

    SListPrint(plist);

    // 1、直接传递的直接提指针,不传递下标
    // 想要删除数据等于3的节点
    SLTNode *pos = SListFind(plist, 3);

    // 如果说pos不为空,则删除,为空则说明没找到该结构体
    if (pos) {
        // 找到位置后,在作为参数传过来
        SListErase(&plist, pos);
    }

    SListPrint(plist);

    // 来一遍头删:
    pos = SListFind(plist, 1);

    // 如果说pos不为空,则删除,为空则说明没找到该结构体
    if (pos) {
        // 找到位置后,在作为参数传过来
        SListErase(&plist, pos);
    }
    SListPrint(plist);
}

int main() {
//    TestSList1();
//    TestSList2();
//    TestSList3();
    TestSList4();
    return 0;
}
;