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

目录

1. 链表的概念及优缺点

1.1 链表的概念

1.2 顺序表和链表的优缺点

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

Slist.h

SList.c

Test.c

3. 笔试选择题练习

练习1

练习2

练习3

练习4

练习5

答案及解析

本篇完。(附下篇对应OJ链接)


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

本篇完。(附下篇对应OJ链接)

下一篇是链表有关的力扣OJ题,穿越回来贴个链接:

数据结构与算法⑤(第二章OJ题,上)前五道链表面试题_GR C的博客-CSDN博客

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值