C 语言数据结构(二):顺序表和链表

news2025/3/11 17:41:15

目录

1. 线性表

2. 顺序表

2.1 概念及结构

2.1.1 静态顺序表(不常用)

2.1.2 动态顺序表(常用)

​编辑

2.2 练习

2.2.1 移除元素

2.2.2 删除有序数组中的重复项

2.2.3 合并两个有序数组

2.3 顺序表存在的问题

3. 链表

3.1 概念及结构

3.2 链表的分类

​编辑

3.2.1 常用链表

3.2.1.1 无头单向非循环链表

3.2.1.2 带头双向循环链表

3.3 无头单向非循环链表的实现

3.4 带头双向循环链表的实现

4. 对比顺序表和链表

💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!

👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!


1. 线性表

线性表(linear list)是 n 个具有相同特性的数据元素的有限序列。它是一种在实际中广泛使用的数据结构。

常见的线性表包括:顺序表、链表、栈、队列、字符串等。

线性表在逻辑上是线性结构,即一条连续的直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。


2. 顺序表

2.1 概念及结构

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般采用数组存储。在数组上完成数据的增删查改。

顺序表一般可以分为:静态顺序表,动态顺序表。

2.1.1 静态顺序表(不常用)

静态顺序表使用定长数组存储元素。存储数据前,需要预先确定数组的长度,且在程序运行过程中数组长度不能改变。

示例:

#include <stdio.h>
#define N 7

// int 类型重命名为 SLDataType
typedef int SLDataType;

// 定义静态顺序表结构体
typedef struct SeqList     // 此结构体类型起了一个别名 SeqList
{
    SLDataType array[N];  // 定长数组
    size_t size;          
    // size_t 用于表示无符号整数类型,通常用于表示数组的索引、对象的大小等
    // size :当前有效数据的个数

} SeqList;

void SeqListInit(SeqList* psl) 
{
    psl->size = 0;   
}

// 向静态顺序表尾部插入数据
int SeqListPushBack(SeqList* psl, SLDataType x) 
{
    if (psl->size >= N) 
    {
        printf("顺序表已满,无法插入\n");
        return 0;
    }

    psl->array[psl->size] = x;
    psl->size++;   // 表示成功插入了一个数据
    return 1;
}

// 打印静态顺序表
void SeqListPrint(SeqList* psl) 
{
    for (size_t i = 0; i < psl->size; i++) 
    {
        printf("%d ", psl->array[i]);
    }
    printf("\n");
}

int main() 
{
    SeqList sl;
    // 声明一个SeqList类型的变量sl,用于表示一个静态顺序表

    SeqListInit(&sl);
    SeqListPushBack(&sl, 1);
    SeqListPushBack(&sl, 2);
    SeqListPushBack(&sl, 3);
    SeqListPrint(&sl);

    return 0;
}

2.1.2 动态顺序表(常用)

动态顺序表使用动态开辟的数组存储元素。可根据实际需要动态地增减数组的容量,以适应数据量的变化。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLDataType;
// int 类型重命名为 SLDataType

typedef struct SeqList   // 此结构体类型起了一个别名 SeqList
{
    SLDataType* array;  // 指向动态开辟的数组
    size_t size;        // 顺序表中有效数据的个数
    size_t capacity;    // 顺序表当前容量大小
} SeqList;

// 顺序表初始化
void SeqListInit(SeqList* psl)
{
    // 检查传入的顺序表指针是否为空,若为空则终止程序
    assert(psl);

    // 初始化时先不分配空间,将数组指针置为 NULL
    psl->array = NULL;
    
    psl->size = 0;
    psl->capacity = 0;
}

// 检查空间,如果满了,进行增容
void CheckCapacity(SeqList* psl)
{
    assert(psl);

    // 当有效数据个数等于容量时,说明顺序表已满,需要增容
    if (psl->size == psl->capacity)
    {
        // 若当前容量为 0,先给一个初始容量,这里设为 4
        size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;

        // 重新分配内存空间,大小为新容量乘以每个数据元素的大小
        SLDataType* tmp = (SLDataType*)realloc(psl->array, newCapacity * sizeof(SLDataType));

        // 检查内存分配是否成功,若失败则终止程序
        assert(tmp);

        // 更新数组指针为新分配的内存地址
        psl->array = tmp;
        
        psl->capacity = newCapacity;
    }
}

// 顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x)
{
    assert(psl);

    // 插入元素前先检查是否需要增容
    CheckCapacity(psl);

    // 在顺序表的末尾插入新元素
    psl->array[psl->size] = x;
    
     // 有效数据个数加 1,表示插入一个元素
    psl->size++;
}

// 顺序表尾删
void SeqListPopBack(SeqList* psl)
{
    assert(psl);

    // 检查顺序表中是否有数据,若没有则终止程序
    assert(psl->size > 0);

    // 表示删除一个元素
    psl->size--;
}

// 顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x)
{
    assert(psl);

    // 插入元素前先检查是否需要增容
    CheckCapacity(psl);

    // 将顺序表中所有元素依次向后移动一位
    for (int i = psl->size; i > 0; i--)
    {
        psl->array[i] = psl->array[i - 1];
    }

    psl->array[0] = x;
    
    psl->size++;
}

// 顺序表头删
void SeqListPopFront(SeqList* psl)
{
    assert(psl);

    // 检查顺序表中是否有数据,若没有则终止程序
    assert(psl->size > 0);

    // 将顺序表中所有元素依次向前移动一位
    for (int i = 0; i < psl->size - 1; i++)
    {
        psl->array[i] = psl->array[i + 1];
    }
    
    psl->size--;
}

// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x)
{
    assert(psl);
    
    for (size_t i = 0; i < psl->size; i++)
    {
        // 若找到与目标元素相等的元素,返回其下标
        if (psl->array[i] == x)
        {
            return (int)i;
        }
    }
    
    return -1;
}

// 顺序表在 pos 位置插入 x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
    assert(psl);

    // 检查插入位置是否合法,即不能小于 0 且不能大于当前有效数据个数
    assert(pos >= 0 && pos <= psl->size);

    // 插入元素前先检查是否需要增容
    CheckCapacity(psl);

    // 将 pos 位置及之后的元素依次向后移动一位
    for (size_t i = psl->size; i > pos; i--)
    {
        psl->array[i] = psl->array[i - 1];
    }
    
    psl->array[pos] = x;
    
    psl->size++;
}

// 顺序表删除 pos 位置的值
void SeqListErase(SeqList* psl, size_t pos)
{
    assert(psl);

    assert(pos >= 0 && pos < psl->size);
    
    for (size_t i = pos; i < psl->size - 1; i++)
    {
        psl->array[i] = psl->array[i + 1];
    }
    
    psl->size--;
}

// 顺序表销毁
void SeqListDestory(SeqList* psl)
{
    assert(psl);

    // 释放动态分配的数组内存
    free(psl->array);

    // 将数组指针置为 NULL,防止悬空指针
    psl->array = NULL;
    
    psl->size = 0;
    
    psl->capacity = 0;
}

void SeqListPrint(SeqList* psl)
{
    assert(psl);
    
    for (size_t i = 0; i < psl->size; i++)
    {
        printf("%d ", psl->array[i]);
    }
    printf("\n");
}

int main()
{
    SeqList sl;
    // 声明一个SeqList类型的变量sl,用于表示一个动态顺序表

    // 表尾插
    SeqListInit(&sl);
    SeqListPushBack(&sl, 1);
    SeqListPushBack(&sl, 2);
    SeqListPushBack(&sl, 3);
    SeqListPrint(&sl);

    // 表头插
    SeqListPushFront(&sl, 0);
    SeqListPrint(&sl);

    // 查找
    int index = SeqListFind(&sl, 2);
    if (index != -1)
    {
        printf("元素 2 在顺序表中的下标是: %d\n", index);
    }
    else
    {
        printf("未找到元素 2\n");
    }

    // 表尾删
    SeqListPopBack(&sl);
    SeqListPrint(&sl);

    // 表头删
    SeqListPopFront(&sl);
    SeqListPrint(&sl);

    // 指定位置插入值
    SeqListInsert(&sl, 1, 10);
    SeqListPrint(&sl);

    // 指定位置删除值
    SeqListErase(&sl, 1);
    SeqListPrint(&sl);

    // 顺序表销毁
    SeqListDestory(&sl);

    return 0;
}

2.2 练习

2.2.1 移除元素

有一个数组和一个值 val,要求:

  • 原地移除所有等于 val 的元素
  • 返回数组中不等于 val (剩余)的元素的数量 k
  • 新数组前 k 个元素包含不等于 val 的元素

“原地”:仅使用常数级别的额外空间,即空间复杂度为 O(1)。其算法直接在原始数据存储的内存位置上进行修改,而不是创建一个与原始数据规模相当的新数据结构来存储处理后的结果。

#include <stdio.h>

int removeElement(int* nums, int numsSize, int val) 
{
    int k = 0;
    
    for (int i = 0; i < numsSize; i++) 
    {
        if (nums[i] != val) 
        {
            // 如果不等于 val,则将该元素放到数组的前 k 个位置
            nums[k] = nums[i];
            
            k++;
        }
    }
    
    return k;
}

int main() 
{
    int nums[] = {3, 2, 2, 3};
    int numsSize = sizeof(nums) / sizeof(nums[0]);
    int val = 3;

    int newSize = removeElement(nums, numsSize, val);
    printf("剩余元素的数量: %d\n", newSize);
    
    printf("移除指定元素后的数组: ");
    for (int i = 0; i < newSize; i++) 
    {
        printf("%d ", nums[i]);
    }
    printf("\n");

    return 0;
}

只遍历数组一次,所以时间复杂度 O(n) ,n 为数组长度。

2.2.2 删除有序数组中的重复项

原地删除一个非严格递增排列的数组 nums 中重复的元素,使每个元素只出现一次,保持元素的相对顺序不变,返回新数组的长度 k ,新数组前 k 个元素包含唯一元素。

#include <stdio.h>

int removeDuplicates(int* nums, int numsSize) 
{
    if (numsSize == 0) 
    {
        return 0;
    }

    int slow = 0;

    for (int fast = 1; fast < numsSize; fast++) 
    {
        if (nums[fast] != nums[slow]) 
        {
            slow++;
            nums[slow] = nums[fast];
        }
    }

    return slow + 1;
}

int main() 
{
    int nums[] = {1, 1, 2, 2, 2, 3, 4, 4, 5};
    int numsSize = sizeof(nums) / sizeof(nums[0]);

    int newLength = removeDuplicates(nums, numsSize);

    printf("删除重复元素后数组的新长度: %d\n", newLength);
    printf("删除重复元素后的数组: ");
    for (int i = 0; i < newLength; i++) 
    {
        printf("%d ", nums[i]);
    }
    printf("\n");

    return 0;
}

只遍历数组一次,所以时间复杂度 O(n) ,n 为数组长度。

2.2.3 合并两个有序数组

给定两个非递减排序的整数数组 nums1 和 nums2,及整数 m 和 n

要求:

  • nums1 初始长度是 m + n,前 m 个是待合并元素,后 n 个为 0 ;
  • nums2 初始长度为 n;
  • nums2 合并到 nums1 里,合并后数组保持非递减顺序,合并后结果存于 nums1 中。
#include <stdio.h>

void merge(int* nums1, int m, int* nums2, int n) 
{
    int p1 = m - 1;  // nums1 中最后一个“有效”元素的索引
    int p2 = n - 1;  // nums2 中最后一个元素的索引
    int p = m + n - 1;  // 合并后数组最后一个元素的索引

    // 从后往前比较两个数组的元素
    while (p1 >= 0 && p2 >= 0) 
    {
        if (nums1[p1] > nums2[p2]) 
        {
            nums1[p] = nums1[p1];
            p1--;
        }
        else 
        {
            nums1[p] = nums2[p2];
            p2--;
        }
        p--;
    }

    // 其中一个数组遍历完后,若 nums2 还有剩余元素,将其复制到 nums1 前面
    while (p2 >= 0) 
    {
        nums1[p] = nums2[p2];
        p2--;
        p--;
    }
}

int main() 
{
    int nums1[] = { 1, 2, 3, 0, 0, 0, 0, 0 };
    int m = 3;
    int nums2[] = { 0, 2, 5, 6, 7 };
    int n = 5;

    merge(nums1, m, nums2, n);

    for (int i = 0; i < m + n; i++) 
    {
        printf("%d ", nums1[i]);
    }
    printf("\n");

    return 0;
}

对比nums1[p]每次比较后的数组
nums1[7] = 71,2,3,0,0,0,0,7
nums1[6] = 61,2,3,0,0,0,6,7
nums1[5] = 51,2,3,0,0,5,6,7
nums1[4] = 31,2,3,0,3,5,6,7
nums1[3] = 21,2,3,2,3,5,6,7
nums1[2] = 21,2,2,2,3,5,6,7
nums1[1] = 11,1,2,2,3,5,6,7

然后 p1 = -1,p2 = 0,p = 0,执行第二个 while 循环,nums1[0] = nums2[0] = 0 ,得到合并数组{0,1,2,2,3,5,6,7} 。

另外,只遍历数组 nums1 和 nums2 一次,所以时间复杂度为 O(m+n) 。

2.3 顺序表存在的问题

顺序表存在以下问题:

  • 进行中间或表头插入、删除操作时,时间复杂度达 O (N)。
  • 增容要申请新空间、拷贝数据、释放旧空间,消耗大。
  • 扩容一般扩大为原来的 2 倍,若插入数据少,造成空间浪费。

为解决这些问题,下面介绍链表结构。


3. 链表

3.1 概念及结构

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

链表在逻辑上是连续的,但是——链表节点一般从堆上申请空间,而堆上空间的分配依据一定策略进行,导致两次申请的空间可能连续,也可能不连续——所以链表的物理存储地址不一定连续。

3.2 链表的分类

  • 单向/双向

  • 带头/不带头

  • 循环/非循环

3.2.1 常用链表

3.2.1.1 无头单向非循环链表

结构简单,一般不会单独用来存数据。实际中更多作为其他数据结构的子结构,如哈希桶、图的邻接表等。另外这种结构在笔试面试中较常见

3.2.1.2 带头双向循环链表

结构最复杂,一般用来单独存储数据。实际中使用的链表数据结构多是带头双向循环链表。

3.3 无头单向非循环链表的实现

#include <stdio.h>
#include <stdlib.h>

typedef int SLTDateType;

// 定义链表节点结构体
typedef struct SListNode
{
    SLTDateType data;  // 节点存储的数据
    struct SListNode* next;  // 指向下一个节点的指针
}SListNode;

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x) 
{
    // 为新节点分配内存空间
    SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
    if (newNode == NULL) 
    {
        perror("malloc");
        return NULL;
    }
    // 初始化新节点的数据
    newNode->data = x;

    // 新节点的下一个节点初始化为空
    newNode->next = NULL;

    return newNode;
}

void SListPrint(SListNode* plist) 
{
    SListNode* cur = plist;
    // plist 是指向链表头节点的指针

    while (cur != NULL) 
    // 无头单向非循环链表的末尾节点的 next 指针始终被设置为 NULL
    {
        // 打印当前节点的数据
        printf("%d -> ", cur->data);
        // 移动到下一个节点
        cur = cur->next;
    }
    // 打印链表结束标志
    printf("NULL\n");
}

// 尾插
void SListPushBack(SListNode** pplist, SLTDateType x) 
{
    // 创建新节点
    SListNode* newNode = BuySListNode(x);
    if (*pplist == NULL) 
    {
        // 如果链表为空,新节点就是链表的头节点
        *pplist = newNode;
    }
    else 
    {
        // 找到链表的尾节点
        SListNode* tail = *pplist;
        while (tail->next != NULL) 
        {
            tail = tail->next;
        }
        
        tail->next = newNode;
    }
}

// 头插
void SListPushFront(SListNode** pplist, SLTDateType x) 
{
    SListNode* newNode = BuySListNode(x);

    // 新节点的下一个节点指向原来的头节点
    newNode->next = *pplist;
    
    *pplist = newNode;
}

// 尾删
void SListPopBack(SListNode** pplist) 
{
    if (*pplist == NULL)   // 若链表为空 
    {
        return;   // 函数立即终止执行
    }
    if ((*pplist)->next == NULL) 
    {
        // 如果链表只有一个节点,释放该节点并将头指针置空
        free(*pplist);
        *pplist = NULL;
    }
    else 
    {
        SListNode* prev = NULL;
        SListNode* tail = *pplist;
        while (tail->next != NULL) 
        {
            prev = tail;
            tail = tail->next;
        }
        // 最后一次循环中,prev 被赋为 tail 的前一个节点,tail 移到了尾节点
        
        free(tail);

        // 倒数第二个节点的下一个节点置空
        prev->next = NULL;
    }
}

// 头删
void SListPopFront(SListNode** pplist) 
{
    if (*pplist == NULL) 
    {
        return;
    }
    // 保存原来的头节点
    SListNode* first = *pplist;
    // 更新头节点为原来头节点的下一个节点
    *pplist = first->next;
    // 释放原来的头节点
    free(first);
}

// 查找
SListNode* SListFind(SListNode* plist, SLTDateType x) 
{
    // 遍历链表,查找值为x的节点
    SListNode* cur = plist;
    while (cur != NULL) 
    {
        if (cur->data == x) 
        {
            return cur;
        }
        
        cur = cur->next;
    }
    
    return NULL;
}

// 在pos的后一个位置插入值 x
void SListInsertAfter(SListNode* pos, SLTDateType x) 
{
    if (pos == NULL) 
    {
        return;
    }
    
    SListNode* newNode = BuySListNode(x);
    
    newNode->next = pos->next;
    
    pos->next = newNode;
}

// 删除pos后一个位置的值
void SListEraseAfter(SListNode* pos) 
{
    if (pos == NULL || pos->next == NULL)   // 如果pos为空或者pos是尾节点 
    {
        return;
    }
    // 保存pos节点的下一个节点
    SListNode* nextNode = pos->next;
    // pos节点的下一个节点指向要删除节点的下一个节点
    pos->next = nextNode->next;
    free(nextNode);
}

int main() 
{
    SListNode* list = NULL;   
    // 声明并初始化一个指向 SListNode 类型结构体的指针 list
    // 用于表示初始为空的链表

    // 尾插
    SListPushBack(&list, 1);
    SListPushBack(&list, 2);
    SListPushBack(&list, 3);
    SListPrint(list);

    // 头插
    SListPushFront(&list, 0);
    SListPrint(list);

    // 尾删
    SListPopBack(&list);
    SListPrint(list);

    // 头删
    SListPopFront(&list);
    SListPrint(list);

    // 查找
    SListNode* found = SListFind(list, 2);
    if (found) 
    {
        printf("Found: %d\n", found->data);
    }
    else 
    {
        printf("Not found\n");
    }
    // 在pos的后一个位置插入值
    SListInsertAfter(list, 4);
    SListPrint(list);
    // 删除pos后一个位置的值
    SListEraseAfter(list);
    SListPrint(list);

    return 0;
}

无头单向非循环链表的末尾节点的 next 指针始终被设置为 NULL”:

  • 节点创建:新节点创建时,next 指针初始化为 NULL
  • 尾插操作:尾插新节点时,若链表为空,新节点成头节点,next 为 NULL;若不为空,找到尾节点,将其 next 指向新节点(新节点 next 已初始化为 NULL)。
  • 尾删操作:尾删时,若链表仅一个节点,释放后链表为空;若有多个节点,释放尾节点,将倒数第二个节点的 next 置为 NULL

思考:为什么在pos的一个位置插入值,而非一个位置?为什么不删除 pos 位置节点?

(1)单向链表只能从前向后遍历,没有指向前一节点的指针,无法直接获取 pos 节点的前一个节点,因此只能从头节点开始依次遍历链表,直到找到 pos 节点的前一个节点,时间复杂度达 O(n);而在 pos 后插入,只改两个指针,时间复杂度为 O(1)。

(2)删除 pos 位置节点需找到其前一节点,时间复杂度达 O(n);而删除 pos 后节点,只改一个指针,时间复杂度为 O(1)。

3.4 带头双向循环链表的实现

#include <stdio.h>
#include <stdlib.h>

typedef int LTDataType;

typedef struct ListNode
{
    LTDataType _data;  // 节点存储的数据
    struct ListNode* next;  // 指向下一个节点的指针
    struct ListNode* prev;  // 指向前一个节点的指针
}ListNode;

// 创建返回链表的头结点
ListNode* ListCreate() 
{
    // 为头节点分配内存
    ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
    if (phead == NULL) 
    {
        perror("malloc fail");
        return NULL;
    }
    // 头节点的 next 和 prev 都指向自身,形成循环
    phead->next = phead;
    phead->prev = phead;
    return phead;
}

// 双向链表销毁
void ListDestory(ListNode* plist) 
{
    // 从头节点的下一个节点开始遍历
    ListNode* cur = plist->next;
    while (cur != plist) 
    {
        // 保存当前节点的下一个节点
        ListNode* next = cur->next;
        
        free(cur);
        cur = next;
    }
    // 最后释放头节点的内存
    free(plist);
}

void ListPrint(ListNode* plist) 
{
    ListNode* cur = plist->next;
    while (cur != plist) 
    {
        printf("%d ", cur->_data);
        cur = cur->next;
    }
    printf("\n");
}

// 尾插
void ListPushBack(ListNode* plist, LTDataType x) 
{
    // 创建新节点
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
    if (newnode == NULL) 
    {
        perror("malloc fail");
        return;
    }
    newnode->_data = x;
    // 找到尾节点
    ListNode* tail = plist->prev;
    // 插入新节点
    tail->next = newnode;
    newnode->prev = tail;
    newnode->next = plist;
    plist->prev = newnode;
}

// 尾删
void ListPopBack(ListNode* plist) 
{
    // 检查链表是否为空
    if (plist->next == plist) 
    {
        return;
    }
    // 找到尾节点
    ListNode* tail = plist->prev;
    // 找到尾节点的前一个节点
    ListNode* prev = tail->prev;
    
    free(tail);

    // 更新指针
    prev->next = plist;
    plist->prev = prev;
}

// 头插
void ListPushFront(ListNode* plist, LTDataType x) 
{
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
    if (newnode == NULL) 
    {
        perror("malloc fail");
        return;
    }
    newnode->_data = x;
    // 保存头节点的下一个节点
    ListNode* first = plist->next;
    // 插入新节点
    plist->next = newnode;
    newnode->prev = plist;
    newnode->next = first;
    first->prev = newnode;
}

// 头删
void ListPopFront(ListNode* plist) 
{
    // 检查链表是否为空
    if (plist->next == plist) 
    {
        return;
    }
    // 保存头节点的下一个节点
    ListNode* first = plist->next;
    // 保存头节点的下下一个节点
    ListNode* second = first->next;
    // 释放头节点的下一个节点的内存
    free(first);
    // 更新指针
    plist->next = second;
    second->prev = plist;
}

// 查找
ListNode* ListFind(ListNode* plist, LTDataType x) 
{
    // 从头节点的下一个节点开始遍历
    ListNode* cur = plist->next;
    while (cur != plist) 
    {
        if (cur->_data == x) 
        {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

// 在pos的前一个位置插入
void ListInsert(ListNode* pos, LTDataType x) 
{
    ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
    if (newnode == NULL) 
    {
        perror("malloc fail");
        return;
    }
    newnode->_data = x;
    // 找到 pos 节点的前一个节点
    ListNode* prev = pos->prev;
    // 插入新节点
    prev->next = newnode;
    newnode->prev = prev;
    newnode->next = pos;
    pos->prev = newnode;
}

// 删除pos位置的节点
void ListErase(ListNode* pos) 
{
    ListNode* prev = pos->prev;
   
    ListNode* next = pos->next;
    
    free(pos);

    // 更新指针
    prev->next = next;
    next->prev = prev;
}

int main() 
{
    ListNode* plist = ListCreate();
    // 调用 ListCreate 创建带头双向循环链表,并将返回的头节点指针赋值给 plist

    ListPushBack(plist, 1);
    ListPushBack(plist, 2);
    ListPushBack(plist, 3);
    ListPrint(plist);

    ListPushFront(plist, 0);
    ListPrint(plist);

    ListPopBack(plist);
    ListPrint(plist);

    ListPopFront(plist);
    ListPrint(plist);

    ListNode* pos = ListFind(plist, 2);
    if (pos) 
    {
        ListInsert(pos, 4);
        ListPrint(plist);
    }

    pos = ListFind(plist, 4);
    if (pos) 
    {
        ListErase(pos);
        ListPrint(plist);
    }

    ListDestory(plist);

    return 0;
}


4. 对比顺序表和链表

对比维度顺序表链表
存储空间物理存储上一定连续逻辑上数据元素连续,但物理内存中不一定连续,节点通过指针相互连接
随机访问支持随机访问,通过下标可直接定位元素,时间复杂度为 O (1)需从表头开始,逐个通过指针遍历查找元素,时间复杂度为 O (N)
插入和删除操作在任意位置插入或删除元素时,可能需移动后续元素,效率较低,时间复杂度为 O (N)插入或删除元素时,仅需修改相关节点的指针指向,操作相对简便
插入特性动态顺序表空间不足时,需进行扩容,涉及重新分配内存、数据拷贝等步骤没有容量限制,可随时动态创建和删除节点
应用场景适合元素高效存储且频繁访问的场景,如数组实现的线性表适用于数据插入和删除操作频繁的场景,如实现队列、栈等数据结构
缓存利用率物理存储连续,符合局部性原理,读取数据时相邻数据易一同加载到缓存中,缓存利用率高节点物理位置分散,难以充分利用缓存,缓存利用率较低

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2313324.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

无公网IP也能远程控制Windows:Linux rdesktop内网穿透实战

文章目录 前言1. Windows 开启远程桌面2. Linux安装rdesktop工具3. Win安装Cpolar工具4. 配置远程桌面地址5. 远程桌面连接测试6. 设置固定远程地址7. 固定地址连接测试 前言 如今远程办公已经从一种选择变成了许多企业和个人的必修课&#xff0c;而如何在Linux系统上高效地访…

uniapp+Vue3 开发小程序的下载文件功能

小程序下载文件&#xff0c;可以先预览文件内容&#xff0c;然后在手机上打开文件的工具中选择保存。 简单示例&#xff1a;&#xff08;复制到HBuilder直接食用即可&#xff09; <template><view class"container-detail"><view class"example…

blazemeter工具使用--用于自动生成jmeter脚本并进行性能测试

1、安装blazemeter&#xff08;网上有很多详情的教程&#xff09; 2、开始录制&#xff1a;设置号你的文件名称后开始录制 3、录制完成后保存为jmeter(jmx)文件 4、在jmeter中打开文件 5、添加一个后置处理器&#xff1a;查看结果树&#xff0c;后运行看看能否成功&#xf…

通义万相2.1:开启视频生成新时代

文章摘要&#xff1a;通义万相 2.1 是一款在人工智能视频生成领域具有里程碑意义的工具&#xff0c;它通过核心技术的升级和创新&#xff0c;为创作者提供了更强大、更智能的创作能力。本文详细介绍了通义万相 2.1 的背景、核心技术、功能特性、性能评测、用户反馈以及应用场景…

如何用HTML5 Canvas实现电子签名功能✍️

&#x1f916; 作者简介&#xff1a;水煮白菜王&#xff0c;一位资深前端劝退师 &#x1f47b; &#x1f440; 文章专栏&#xff1a; 前端专栏 &#xff0c;记录一下平时在博客写作中&#xff0c;总结出的一些开发技巧和知识归纳总结✍。 感谢支持&#x1f495;&#x1f495;&a…

区块链中的数字签名:安全性与可信度的核心

数字签名是区块链技术的信任基石&#xff0c;它像区块链世界的身份证和防伪标签&#xff0c;确保每一笔交易的真实性、完整性和不可抵赖性。本文会用通俗的语言&#xff0c;带你彻底搞懂区块链中的数字签名&#xff01; 文章目录 1. 数字签名是什么&#xff1f;从现实世界到区块…

表格columns拼接两个后端返回的字段(以umi框架为例)

在用组件对前端项目进行开发时&#xff0c;我们会遇到以下情况&#xff1a;项目原型中有取值范围这个表字段&#xff0c;需要存放最小取值到最大取值。 而后端返回给我们的数据是返回了一个最小值和一个最大值&#xff0c; 在columns中我们需要对这两个字段进行拼接&#xff0…

sparkTTS window 安装

SparkTTS 的简介 Spark-TTS是一种基于SpardAudio团队提出的 BiCodec 构建的新系统&#xff0c;BiCodec 是一种单流语音编解码器&#xff0c;可将语音策略性地分解为两种互补的标记类型&#xff1a;用于语言内容的低比特率语义标记和用于说话者特定属性的固定长度全局标记。这种…

【K8S系列】深入探究Kubernetes中查看日志的方法

在Kubernetes&#xff08;简称K8s&#xff09;的世界里&#xff0c;日志是诊断和排查问题的关键线索。无论是应用程序的运行状态、错误信息&#xff0c;还是系统的健康状况&#xff0c;都能从日志中找到蛛丝马迹。本文将详细介绍在K8s中查看日志的各种方法&#xff0c;从基础的…

JmeterHttp请求头管理出现Unsupported Media Type问题解决

JmeterHttp请求头管理出现Unsupported Media Type问题解决 大多数的app与pc端压测的时候都会出现这种情况 当我们在jemter测试当中当中遇见Unsupported Media Type&#xff0c;有一种可能就是我们请求的网页的content-Type的类型与我们测试的时候的类型不一致 解决方法 可以添…

十大数据科学Python库

十大数据科学Python库 1、NumPy&#xff1a;脊髓2、Pandas&#xff1a;数据操纵专家3、Matplotlib&#xff1a;艺术之魂4、Scikit-Learn&#xff1a;瑞士军刀5、TensorFlow&#xff1a;聪明的家伙6、PyTorch&#xff1a;叛逆者7、Selenium&#xff1a;操纵大师8、NLTK&#xff…

LabVIEW伺服阀高频振动测试

在伺服阀高频振动测试中&#xff0c;闭环控制系统的实时性与稳定性至关重要。针对用户提出的1kHz控制频率需求及Windows平台兼容性问题&#xff0c;本文重点分析NI PCIe-7842R实时扩展卡的功能与局限性&#xff0c;并提供其他替代方案的综合对比&#xff0c;以帮助用户选择适合…

解决asp.net mvc发布到iis下安全问题

解决asp.net mvc发布到iis下安全问题 环境信息1.The web/application server is leaking version information via the "Server" HTTP response2.确保您的Web服务器、应用程序服务器、负载均衡器等已配置为强制执行Strict-Transport-Security。3.在HTML提交表单中找不…

CSS-基础选择器,字体属性,文本属性介绍

一、CSS 简介 CSS 是层叠样式表 ( Cascading Style Sheets ) 的简称. 有时我们也会称之为 CSS 样式表或级联样式表&#xff61; CSS 是也是一种标记语言 CSS 主要用于设置 HTML 页面中的文本内容(字体&#xff64;大小&#xff64;对齐方式等)&#xff64;图片的外形(宽高&a…

vtkDepthSortPolyData 根据相机视图方向对多边形数据进行排序

1. 作用 在 3D 渲染中&#xff0c;透明对象的渲染顺序非常重要。如果透明对象的渲染顺序不正确&#xff0c;可能会导致错误的视觉效果&#xff08;例如&#xff0c;远处的透明对象遮挡了近处的透明对象&#xff09;。vtkDepthSortPolyData 通过对多边形数据进行深度排序&#…

【MySQL_04】数据库基本操作(用户管理--配置文件--远程连接--数据库信息查看、创建、删除)

文章目录 一、MySQL 用户管理1.1 用户管理1.11 mysql.user表详解1.12 添加用户1.13 修改用户权限1.14 删除用户1.15 密码问题 二、MySQL 配置文件2.1 配置文件位置2.2 配置文件结构2.3 常用配置参数 三、MySQL远程连接四、数据库的查看、创建、删除4.1 查看数据库4.2 创建、删除…

牛客网刷题(5)(HTML之元素<input>、表格<table>与描述列表<dl>、元素<label>)

目录 一、哪种输入类型定义滑块控件&#xff1f;元素&#xff08;input&#xff09; &#xff08;1&#xff09;官方解析。 &#xff08;2&#xff09;总结。 &#xff08;3&#xff09;牛客大佬总结。 &#xff08;4&#xff09;HTML5——元素&#xff08;input&#xff09;的…

IDEA(十一)调整新版本的工具栏显示Git操作(pull、commit、push、revert等)

目录 一、背景二、操作步骤2.1 开启新 UI 样式2.2 设置 Tool Window 工具栏 一、背景 好久没有更新 IDEA 了&#xff0c;更新之后发现 IDEA 的工具栏消失了。一番操作之后&#xff0c;终于把 IDEA 的工具栏的设置调整好了&#xff0c;在此进行记录调整步骤&#xff0c;供大家学…

C++编程:进阶阶段—4.2对象

目录 4.2 对象特征 4.2.1 构造函数和析构函数 4.2.2 构造函数的分类 4.2.3 拷贝函数调用时机 4.2.4 构造函数调用规则 4.2.5 深拷贝与浅拷贝 4.2.6 初始化列表 4.2.7 类对象作为类成员 4.2.8 静态成员 4.2.9 成员变量和成员函数的存储 4.2.10 this指针 4.2.11 空指针…

TensorFlow.js 全面解析:在浏览器中构建机器学习应用

TensorFlow.js 全面解析&#xff1a;在浏览器中构建机器学习应用 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;可以分享一下给大家。点击跳转到网站。 https://www.captainbed.cn/ccc 文章目录 TensorFlow.js 全面解析&#x…