数据结构与算法④(第二章下)链表概念+单链表的实现

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 数据结构与算法④(第二章下)链表概念+单链表的实现

1. 链表的概念及优缺点

1.1 链表的概念

链表是一种物理存储结构上非连续、非顺序的存储结构,

数组元素的逻辑顺序是通过链表中的指针链接次序实现的。

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向、双向

2. 带头、不带头

3. 循环、非循环


1. 无头单向非循环链表:(下面实现的就是这种结构)

结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构

如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

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

都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

1.2 顺序表和链表的优缺点

为什么顺序表和链表各自存在?

因为他们各有优点,他们是互补的,并不是有他没我,有我没他的。


顺序表的优点:

① 支持随机访问,有些算法需要结构随机访问,比如二分查找和优化的快排等。

② 数据是按顺序存放的,空间利用率高。

③ 通过下标直接访问,存取速度高效。

顺序表的缺点:

① 中间/头部的插入删除,需要挪动,挪动数据时也是存在消耗的,时间复杂度为O(N)

② 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。

③ 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,

我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。


链表的优点:① 按需申请空间,不用就释放空间( 链表可以更合理地使用空间)。

② 头部或者中间位置的插入和删除,不需要挪动数据。

③ 不存在空间浪费。

链表的缺陷:

① 每一个数据,都要存一个指针去链接后面的数据节点。


② 不支持随机访问(用下标直接访问第 i 个),必须得走 O(N) 。


单链表的缺陷还是很多的,单纯单链表的增删查找的意义不大,

但是很多OJ题考察的都是单链表。单链表多用于更复杂的数据结构的子结构,

如哈希桶,邻接表等。所以真正存储数据还是得靠双向链表。

2. 链表的实现(完整代码)

还是和上一篇顺序表的实现一样,主要实现增删查改功能,可以一个一个函数看,很简单。

Slist.h

 
#pragma once
 
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
 
typedef int SLNDataType;
 
typedef struct SlistNode          //single单一的  node节点
{
    SLNDataType data;
    struct SlistNode* next;
}SLNode;
 
void SListPrint(SLNode* phead);
void SListPushBack(SLNode** pphead, SLNDataType x);
void SListPushFront(SLNode** pphead, SLNDataType x);
SLNode* CreateNewNode(SLNDataType x);
void SListPopBack(SLNode** pphead);
void SListPopFront(SLNode** pphead);
SLNode* SListFind(SLNode* phead, SLNDataType x);
void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x);
void SListInsertAfter(SLNode* pos, SLNDataType x);
void SListEarse(SLNode** pphead, SLNode* pos);
void SListEarseAfter(SLNode* pos);
void SListDestroy(SLNode** pphead);


SList.c

 
#include"SList.h"
 
void SListPrint(SLNode* phead)
{
    SLNode* cur = phead;
    while (cur != NULL)
    {
        printf("%d->", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}
 
void SListPushBack(SLNode** pphead, SLNDataType x)
{
    SLNode* NewNode = CreateNewNode(x);
 
    if (*pphead == NULL)
    {
        *pphead = NewNode;
    }
    else
    {
        SLNode* tail = *pphead;//tail 尾巴
        while (tail->next != NULL)//找到尾节点
        {
            tail = tail->next;
        }
        tail->next = NewNode;
    }
}
 
SLNode* CreateNewNode(SLNDataType x)
{
    SLNode* NewNode = (SLNode*)malloc(sizeof(SLNode));
    if (NewNode == NULL)
    {
        printf("malloc failed!\n");
        exit(-1);
    }
    NewNode->data = x;
    NewNode->next = NULL;
    return NewNode;
}
 
void SListPushFront(SLNode** pphead, SLNDataType x)
{
    SLNode* NewNode = CreateNewNode(x);
 
    NewNode->next = *pphead;
    *pphead = NewNode;
}
 
void SListPopBack(SLNode** pphead)
{
    assert(*pphead != NULL);
    if ((*pphead)->next == NULL)//如果只有一个节点
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
        SLNode* tail = *pphead;
        while (tail->next->next)
        {
            tail = tail->next;
        }
        free(tail->next);
        tail->next = NULL;
    }
}
 
void SListPopFront(SLNode** pphead)
{
    assert(*pphead != NULL);
 
    SLNode* begin = *pphead;
    *pphead = (*pphead)->next;
    free(begin);
    begin = NULL;
}
 
SLNode* SListFind(SLNode* phead, SLNDataType x)
{
    SLNode* cur = phead;
    while (cur != NULL)
    {
        if (cur->data == x)
        {
            return cur;
        }
        else
        {
            cur = cur->next;
        }
    }
    return NULL;
}
 
void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
    SLNode* NewNode = CreateNewNode(x);
    if (*pphead == pos)
    {
        NewNode->next = *pphead;
        *pphead = NewNode;
    }
    else
    {
        SLNode* posPrev = *pphead;
        while (posPrev->next != pos)
        {
            posPrev = posPrev->next;
        }
        posPrev->next = NewNode;
        NewNode->next = pos;
    }
}
 
void SListInsertAfter(SLNode* pos, SLNDataType x)
{
    assert(pos != NULL);
    SLNode* NewNode = CreateNewNode(x);
    NewNode->next = pos->next;
    pos->next = NewNode;
    //和前面对比,单链表的缺陷是要找pos的前一个要从头往后找
}
 
void SListEarse(SLNode** pphead, SLNode* pos)
{
    assert(*pphead != NULL);
    assert(pos != NULL);
    if (*pphead == pos)//如果是头删
    {
        *pphead = pos->next;
        free(pos);
        pos = NULL;
    }
    else
    {
        SLNode* posPrev = *pphead;
        while (posPrev->next != pos)
        {
            posPrev = posPrev->next;
        }
        posPrev->next = pos->next;
        free(pos);
        pos = NULL;//可以不写,形参不会改变实参,这个野指针会被栈帧销毁,但是个好习惯emm
    }
}
 
void SListEarseAfter(SLNode* pos)
{
    //1 2 3 4
    assert(pos != NULL);
    assert(pos->next != NULL);
    SLNode* posAfter= pos->next;
    pos->next = posAfter->next;
    free(posAfter);
    posAfter = NULL;//可以不写,形参不会改变实参,这个野指针会被栈帧销毁
}
 
void SListDestroy(SLNode** pphead)
{
    assert(pphead);
    SLNode* cur = *pphead;
    while (cur)
    {
        SLNode* next = cur->next;
        free(cur);
        cur = next;
    }
    *pphead = NULL;
}

Test.c

 
#define _CRT_SECURE_NO_WARNINGS 1
 
#include"SList.h"
 
void TestSList1()//测试尾插头插
{
    SLNode* pList = NULL;
    SListPushBack(&pList, 1);
    SListPushBack(&pList, 2);
    SListPushBack(&pList, 3);
    SListPushBack(&pList, 4);
 
    SListPrint(pList);
 
    SListPushFront(&pList, 1);
    SListPushFront(&pList, 2);
    SListPushFront(&pList, 3);
    SListPushFront(&pList, 4);
 
    SListPrint(pList);
}
 
void TestSList2()//测试尾删头删
{
    SLNode* pList = NULL;
    SListPushBack(&pList, 1);
    SListPushBack(&pList, 2);
    SListPushBack(&pList, 3);
    SListPushBack(&pList, 4);
    SListPrint(pList);
 
    SListPopBack(&pList);
    SListPrint(pList);
 
    SListPopFront(&pList);
    SListPrint(pList);
 
    SListPopBack(&pList);
    SListPopBack(&pList);
    SListPrint(pList);
}
 
void TestSList3()//测试查找函数
{
    SLNode* pList = NULL;
    SListPushFront(&pList, 1);
    SListPushFront(&pList, 2);
    SListPushFront(&pList, 3);
    SListPushFront(&pList, 2);
    SListPushFront(&pList, 4);
    SListPushFront(&pList, 2);
    SListPushFront(&pList, 2);
    SListPushFront(&pList, 4);
    SListPrint(pList);
 
    SLNode* pos = SListFind(pList, 2);
    for (int i = 0;pos != NULL;i++)
    {
        printf("第%d个pos节点:%p->%d\n", i, pos, pos->data);
        pos->data = 9;//查找过程中把2改成9  返回SLNode*的好处
        pos = SListFind(pos->next, 2);
    }
    SListPrint(pList);
}
 
void TestSList4()//测试指定位置的前面插入
{
    SLNode* pList = NULL;
    SListPushBack(&pList, 1);
    SListPushBack(&pList, 2);
    SListPushBack(&pList, 3);
    SListPushBack(&pList, 4);
 
    SListPrint(pList);
 
    SLNode* pos = SListFind(pList, 2);
    SListInsert(&pList, pos, 60);
 
    SListPrint(pList);
 
    pos = SListFind(pList, 1);
    SListInsert(&pList, pos, 70);
    SListPrint(pList);
 
    SListInsert(&pList, NULL, 80);
    SListPrint(pList);
}
 
void TestSList5()//测试指定位置的后面插入
{
    SLNode* pList = NULL;
    SListPushBack(&pList, 1);
    SListPushBack(&pList, 2);
    SListPushBack(&pList, 3);
    SListPushBack(&pList, 4);
    SListPrint(pList);
 
    SLNode* pos = SListFind(pList, 3);
    SListInsertAfter(pos, 10);
    SListPrint(pList);
 
    pos = SListFind(pList, 4);
    SListInsertAfter(pos, 20);
    SListPrint(pList);
 
    pos = SListFind(pList, 1);
    SListInsertAfter(pos, 30);
    SListPrint(pList);
}
 
void TestSList6()//测试指定位置的删除
{
    SLNode* pList = NULL;
    SListPushBack(&pList, 1);
    SListPushBack(&pList, 2);
    SListPushBack(&pList, 3);
    SListPushBack(&pList, 4);
    SListPrint(pList);
 
    SLNode* pos = SListFind(pList, 3);
    SListEarse(&pList, pos);
    SListPrint(pList);
 
    pos = SListFind(pList, 1);
    SListEarse(&pList, pos);
    SListPrint(pList);
 
    pos = SListFind(pList, 2);
    SListEarse(&pList, pos);
    SListPrint(pList);
 
    pos = SListFind(pList, 4);
    SListEarse(&pList, pos);
    SListPrint(pList);
}
void TestSList7()//测试指定位置之后的删除和整个链表销毁
{
    SLNode* pList = NULL;
    SListPushBack(&pList, 1);
    SListPushBack(&pList, 2);
    SListPushBack(&pList, 3);
    SListPushBack(&pList, 4);
    SListPrint(pList);
    //SListDestroy(&pList);
    //SListPrint(pList);
 
    SLNode* pos = SListFind(pList, 3);
    SListEarseAfter(pos);
    SListPrint(pList);
 
    pos = SListFind(pList, 1);
    SListEarseAfter(pos);
    SListPrint(pList);
 
    pos = SListFind(pList, 1);
    SListEarseAfter(pos);
    SListPrint(pList);
 
    SListDestroy(&pList);
    SListPrint(pList);
}
 
int main()
{
    //TestSList1();//测试尾插头插
    //TestSList2();//测试尾删头删
    //TestSList3();//测试查找函数
    //TestSList4();//测试指定位置的前面插入
    //TestSList5();//测试指定位置的后面插入
    //TestSList6();//测试指定位置的删除
    TestSList7();//测试指定位置之后的删除和整个链表销毁
    return 0;
}

3. 笔试选择题练习

练习1

下列关于链表的说法哪个是正确的?( )

A.插入或删除时,无需移动其他元素

B.数据在内存中一定是连续的

C.需要事先估计存储空间

D.可以随机访问表内的元素

练习2

在单链表指针为p的结点之后插入指针为s的结点,正确的操作是( )

A.p->next=s; s->next=p->next;

B.p->next=s->next; p->next=s;

C.s->next=p->next; p->next=s;

D.p->next=s->next; p->next=s;


练习3

在一个单链表中,q 的前一个节点为 p,删除 q 所指向节点时,

以下代码正确的执行语句及次序是(  )

①`q->next=p->next;`

②`p->next=q->next;`

③`delete p;`

④`delete q;`

A.②③

B.④

C.①④

D.②④


练习4

在长度为n(n>1)的单链表上,设有头和尾两个指针,执行( )操作与链表的长度有关。

A.在单链表第一个元素前插入一个新元素

B.在单链表最后一个元素后插入一个新元素

C.删除单链表中的第一个元素

D.删除单链表中的最后一个元素


练习5

自己完成下面的接口函数

 
#pragma once
 
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
 
typedef int SLNDataType;
 
typedef struct SlistNode          //single单一的  node节点
{
    SLNDataType data;
    struct SlistNode* next;
}SLNode;
 
void SListPrint(SLNode* phead);
void SListPushBack(SLNode** pphead, SLNDataType x);
void SListPushFront(SLNode** pphead, SLNDataType x);
SLNode* CreateNewNode(SLNDataType x);
void SListPopBack(SLNode** pphead);
void SListPopFront(SLNode** pphead);
SLNode* SListFind(SLNode* phead, SLNDataType x);
void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x);
void SListInsertAfter(SLNode* pos, SLNDataType x);
void SListEarse(SLNode** pphead, SLNode* pos);
void SListEarseAfter(SLNode* pos);
void SListDestroy(SLNode** pphead);

答案及解析

练习1答案:A


解析:链表插入删除元素只需要修改指针指向,不需要移动元素。链表是用指针链接前后节点,数据在内存中不一定连续,每次插入元素,都是先出开一个节点的空间,不需要事先估计存储空间,并且链表没有索引,不能随机访问。


练习2答案:C


解析:先把s和p的next节点链接,再更新p的next为s


练习3答案:D


解析: 首先要更新链接,再删除q节点


练习4答案:D


解析:A选项为头插,不需要遍历链表,B选项为尾插,也不需要遍历链表,C选项为头删,不需要遍历链表,只有D选项,为尾删,需要遍历单链表,找到尾节点的前一个节点。所以与长度有关。


练习5答案参照上面SList.c

本篇完。

目录
相关文章
|
1月前
|
算法 索引
❤️算法笔记❤️-(每日一刷-141、环形链表)
❤️算法笔记❤️-(每日一刷-141、环形链表)
45 0
|
17天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
17天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
1月前
|
存储 缓存 算法
经典算法之链表篇(三)
经典算法之链表篇(三)
|
1月前
|
算法
经典算法之链表篇(二)
经典算法之链表篇(二)
|
1月前
|
算法 索引
经典算法之链表篇
经典算法之链表篇
|
1月前
|
算法
❤️算法笔记❤️-(每日一刷-160、相交链表)
❤️算法笔记❤️-(每日一刷-160、相交链表)
17 1
|
1月前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
30 0
|
14天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
5天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
15 1