初阶数据结构学习记录——셋 单链表(1)

目录

SList.h

SList.c

test.c

1、尾插

2、尾删

3、头部操作

4、test.c


从顺序表中可以看出,实现增删等功能需要一个个挪动,并不简洁,比较繁琐。扩容时,需要整个进行扩容,如果是异地扩容,有一定代价,且可能存在一定空间浪费。

针对这些问题的优化方案就是按需事情释放和不挪动数据。想使用再开辟一个,而要读取这些空间时,遍历并不行,用指针可快速访问,因此也就出现了链表。在链表中,每一块空间就是一个节点。具体以代码呈现。

struct SListNode
{
    int data;
    struct SListNode* next;
}

结构体里放一个指针,用来指向下一个节点.为了清楚地看到链表的结构。我先放上一小段代码。

SList.h

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

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

SLTNode* BuySLTNode(SLTDataType x);
SLTNode* CreateSList(int n);
void SLTPrint(SLTNode* phead);

void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopFront(SLTNode** pphead);

SList.c

#include "SList.h"

SLTNode* BuySLTNode(SLTDataType x)
{
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    if (newnode == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    newnode->data = x;
    newnode->next = NULL;
    return newnode;//这里返回一个指针的地址,自然要用一个指针来接收。
}

SLTNode* CreateSList(int n)
{
    SLTNode* phead = NULL, * ptail = NULL;
    for (int i = 0; i < n; ++i)
    {
        SLTNode* newnode = BuySLTNode(i + 10);
        if (phead == NULL)
        {
            ptail = phead = newnode;
        }
        else
        {
            ptail->next = newnode;
            ptail = newnode;
        }
    }
    return phead;//其实phead也是局部变量,也被销毁了,但是在这之前已经传回去了。函数外负责接收的指针就能拿到整个链表的头位置地址,操作者也就可以使用这个链表了。
}

void SLTPrint(SLTNode* phead)
{
    SLTNode* cur = phead;
    while (cur != NULL)
    {
        //printf("[%d|%p]->", cur->data, cur->next);
        printf("%d->", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}


 

test.c

#include "SList.h"

void TestSList1()
{
        /*SLTNode* n1 = BuySLTNode(1);//为什么要用指针而不是SLTNode sl?因为整个链表每个节点我们需要保存好内容,如果第二种办法,开辟在栈区,那么出了函数数据就没有了,而创建指针,使用malloc,就会存储在堆区,自然也就符合要求了
        SLTNode* n2 = BuySLTNode(2);
        SLTNode* n3 = BuySLTNode(3);
        SLTNode* n4 = BuySLTNode(4);
        n1->next = n2;
        n2->next = n3;
        n3->next = n4;
        n4->next = NULL;*/
    SLTNode* plist = CreateSList(10);
    SLTPrint(plist);
}

int main()
{
    TestSList1();
    return 0;
}

链表有很多种,不过比较核心的就是无头单向非循环链表和带头双向循环链表。这篇文章写无头单向非循环链表。但本篇所写的代码一定是挫的,因为本篇主要在于理解单链表,之后再升级。

刚才上头所写其实就是一个单链表。我们继续写它的插删等功能

1、尾插

事实上,单的尾部动作比较麻烦。尾插我们需要先找到尾,可是我们只有头,所以就需要一点点移过去,但是这样写是错误的

SLTNode* newnode = BuySLTNode(x);
SLTNode* tail = phead;
while (tail != NULL)
{
    tail = tail->next;
}
tail = newnode;

为什么错误?tail->next是NULL的时候,tail被赋值为NULL,然后退出循环,把newnode给了tail,但是那个next并没有连接上newnode。这只是tail自己在改变。当退出整个函数时,tail这个局部变量就没了,newnode也没了,尾插并没有实现。

while (tail->next != NULL)
{
    tail = tail->next;
}
tail->next = newnode;

所以我们需要控制好next和tail才能完成基本的单链表。现在在test.c做个测试

void TestSList2()
{
    SLTNode* plist = CreateSList(5);
    SLTPushBack(&plist, 100);
    SLTPushBack(&plist, 200);
    SLTPushBack(&plist, 300);
    SLTPrint(plist);
}

int main()
{
    TestSList2();
    return 0;
}

输出结果很正确,是我们所想的。不过还有一个问题,如果头是个NULL呢?是NULL仍然可以继续运行,这不耽误什么,我们对尾插函数做一下改动即可

void SLTPushBack(SLTNode* phead, SLTDataType x)
{
    SLTNode* newnode = BuySLTNode(x);
    SLTNode* tail = phead;
    if (phead == NULL)
    {
        phead = newnode;
    }
    else
    {
        //找尾
        while (tail->next != NULL)
        {
            tail = tail->next;
        }
        tail->next = newnode;
    }
}

好,即使这样,phead = NULL开始执行后,打印出来的结果却是NULL。我插入的值呢?这个问题可能容易被忽略掉,plist是个指针,尾插的时候传过去的是plist,这个问题就和要交换两个数值,传过去的数值一样,要想改变plist这个指针的内容,就需要对指针的地址进行操作。所以phead应该是二级指针。

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
    SLTNode* newnode = BuySLTNode(x);
    if (*pphead == NULL)
    {
        *pphead = newnode;
    }
    else
    {
        SLTNode* tail = *pphead;
        //找尾
        while (tail->next)
        {
            tail = tail->next;
        }
        tail->next = newnode;
    }
}

2、尾删

void SLTPopBack(SLTNode** phead)
{
    SLTNode* tail = *phead;
    while (tail->next)
    {
        tail = tail->next;
    }
    free(tail);
    tail = NULL;
}

一段错误的代码。tail一直往后走,走到最后一个时,tail出循环,然后free,再出函数。tai是一个局部变量,出了函数就消失了,但是还在函数内时就已经释放最后指向的空间了。假设插入100,200,300。300所在的空间被释放了,看似已经删除,但是200的next并没有变成NULL,而是还指向第三块空间,这时候它就变成野指针了,因为没有我们没有写改变第二个next的代码。解决方案有两个,一个是双指针走,这样就能有另一个指针指向第二个。或者,while判断里tail->next->next,对后后个进行判断,tail停下来的时候就能停在第二个了。下面是第一个写法.

void SLTPopBack(SLTNode** phead)
{
    SLTNode* tail = *phead;
    SLTNode* prev = NULL;
    while (tail->next)
    {
        prev = tail;
        tail = tail->next;
    }
    free(tail);
    prev->next = NULL;
    tail = NULL;

一次次删除后,又出现了问题,删到还剩最后一个时,这个函数貌似没法正常完成,需要单独处理这个情况,所以

void SLTPopBack(SLTNode** pphead)
{
    assert(*pphead);
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
        SLTNode* tail = *pphead;
        SLTNode* prev = NULL;
        while (tail->next)
        {
            prev = tail;
            tail = tail->next;
        }
        free(tail);
        prev->next = NULL;
    }
}

现在这个尾删没啥问题了。但是呢,总还有失误的地方。如果没记清自己插入了多少数据,删干净后又来了一次,编译器可就难受了。为了应对这个情况,我们断言一下即可assert(*pphead),不过尾插就不需要了。

第二个写法:

void SLTPopBack(SLTNode** pphead)
{
    assert(*pphead);
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
    }
    else
    {
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
    }
}

3、头部操作

链表的头部操作很简单。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
    SLTNode* newnode = BuySLTNode(x);
    newnode->next = *pphead;//即使为空,也不需要单独处理
    *pphead = newnode;//pphead指向新的头;
}

void SLTPopFront(SLTNode** pphead)
{
    assert(*pphead);
    SLTNode* next = (*pphead)->next;
    free(*pphead);
    *pphead = next;//只剩一个时也不需要单独处理
}

头插和头删功能很简单。这也是链表的优势所在,链表的尾部操作其实比较繁琐。

4、test.c

void TestSList3()
{
    SLTNode* plist = NULL;
    SLTPushBack(&plist, 100);
    SLTPushBack(&plist, 200);
    SLTPushBack(&plist, 300);
    SLTPrint(plist);

    SLTPopBack(&plist);
    SLTPrint(plist);

    SLTPopBack(&plist);
    SLTPrint(plist);

    SLTPopBack(&plist);
    SLTPrint(plist);

    //SLTPopBack(&plist);
    //SLTPrint(plist);
}

void TestSList4()
{
    SLTNode* plist = NULL;
    SLTPushFront(&plist, 100);
    SLTPushFront(&plist, 200);
    SLTPushFront(&plist, 300);
    SLTPushFront(&plist, 400);

    SLTPrint(plist);

    SLTPopFront(&plist);
    SLTPrint(plist);
    SLTPopFront(&plist);
    SLTPrint(plist);
    SLTPopFront(&plist);
    SLTPrint(plist);
    SLTPopFront(&plist);
    SLTPrint(plist);
}

int main()
{
    TestSList4();
    return 0;
}

关于pos位置的操作下一篇再写。

结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值