【数据结构】零基础链表教学

前言

        先给大家一个建议,学习数据结构去画图,不会的可以模仿一下我的gif;

        欢迎指正错误;

一.什么是链表

        链表由一个一个的节点组成,节点里面有两部分组成,数据域(date)和指针域(next),数据域呢就是用来储存数据的,而指针域(保存了下一个节点的地址这样就可以用指针访问到了)就是用来找到下一个节点的,一个一个节点链接起来便成了链表了。

       链表就像这火车一样一节连着一节,就跑起来了。

        什么是顺序表呢?这里不介绍我简单概括就是:对数组进去行动态管理。

        链表和顺序表都是线性表,区别是顺序表在逻辑结构和空间结构上都是连续的,而链表则在逻辑结构上连续,在空间结构上一般都不连续

        什么是逻辑结构和空间结构呢?

        我们看这个链表,它们中间都有一个箭头(指针抽象出来的)指向下一个节点,箭头将它节点串起来成了链表,这个箭头是我们抽象出来方便理解的,实际在内存中可没有个箭头指向下一个节点,这种叫逻辑结构

        实际上节点中存储的是下一个节点的指针,没有这个箭头,各个节点在空间中其实是独立的,没有关系的。

        链表也分很多类别,我们先来讲一种结构最简单的链表:单链表

二、单链表

1.静态链表

        为了方便大家理解,先展示个简单的静态链表;

        先定义一个结构体:

typedef struct SLTlistNode SLTNode;
//对结构体重新命名,方便下文使用;
typedef int SLTDateType;
//这里将int命名是为了方便以后更改存储数据的类型
//比如要储存浮点型在这里将int改为float就行了;
struct SLTlistNode {
	SLTNode* next;//指针域保留下个节点的地址
	SLTDateType date;//数据域存储数据;
};

        这是运行的结果: 

        现在我们来详细解释一下这个打印函数

void SLTPrint(SLTNode* phead)
{    
    //这里的phead接收的是first的地址,也就是第一个节点的地址
    SLTNode* cur = phead;
    //实际使用中了我们不会直接去改变phead指针,因为很多情况下我们可能多次要用到头指针;
    while(cur!=NULL)//当cur == NULL时说明我们的链表遍历完毕了,结束循环,常缩写为while(cur);
    {
        printf("%d->",cur->date);
        cur = cur->next;
        //这个代码是核心下面有详细解释;
    }
        printf("NULL\n");
}

                

        好了,经过上面的讲解后应该对链表有大致了解了,现在我们正式开始动态单链表;

        2.动态链表

       (1)创建节点

        有上面的铺垫后我们应该能知道创建一个节点是十分频繁的事情,我们将他封装成一个函数

//创建节点
SLTNode* SLTBuyNode(SLTDateType x)
{
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    assert(newnode);
    //这个是断言函数使用它得包含它的头文件<assert.h>
    //当条件为假时报错,这里用来处理malloc失败的情况;
    newnode->next = NULL;
    newnode->date = x;
    return newnode;
}

           (2)头插和头删

        头插顾名思义从链表的头部插入一个节点,头删则是从头部删除一个节点

        

//头插
void SLTPushFront(SLTNode** pphead,SLTDateType x)
{
    //看到二级指针先别急,等我下文详细解释;
    //先来想一种极端情况,如果链表为空,那么这个节点便是我们的头;
    //但我们发现链表为空下面也行的通,所以不用考虑那种情况了;
    SLTNode* newnode = SLTBuyNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}
        为什么使用二级指针呢?

首先我们知道函数传参有两种:传值传址,传参改变的仅是形参不改变实参 。使用一级指针那么传过来的应该是一个指针变量(head为头节点):

SLTPushFront(head);

         在回到头插上去看,发现我们对这个指针变量是进行了改变的,但它仅仅是一个临时创建的形式参数,出了头插函数后就销毁了,所以没有改变我们的head;

//举个例子
void Example1(int x)
{
    x = 0;
}
void Example2(int* x)
{
    *x = 0;
}
int main()
{
    int a = 6;
    Example1(a);
    printf("%d ",a);
    Example2(&a);
    printf("%d",a);
    return 0;
}
//上面这个程序第一次打印的a的值是6;
//第二次打印的值便为0了;
//这就是传值和传址的差别;

        同样的我们要想改变head,那么就必须得传head的地址,也就是这样:

SLTPushFront(&head);

        它们之间的关系是这样的

        理解以后我们可以引入assert到头插,增强代码健壮性:

void SLTPushFront(SLTNode** pphead,SLTDateType x)
{
    assert(pphead);
    //为什么这里用assert判断因为pphead也就是&head不可能为空;
    //那么*pphead呢,也就是head,它可以为空,空链表头插成为第一个节点呗~~;
    SLTNode* newnode = SLTBuyNode(x);
    newnode->next = *pphead;
    *pphead = newnode;
}

理解了上面这些后你的单链表可以说完成一半了,我们接着往下走:

//头删
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead && *pphead);
    //*pphead不能为空,为空了还删上什么啊~~
    SLTNode* Del = *pphead;//将要删除的节点保存下来,然后将头挪到下一个节点;
    *pphead = *pphead->next;
    free(Del);
    Del = NULL;
    //好习惯:释放置空避免野指针,尽管这里用处不大,但建议养成;
}

        测试一下代码:

    SLTNode* head = NULL;
	SLTPushFront(&head,1);
	SLTPushFront(&head, 2);
	SLTPushFront(&head, 3);
	SLTPushFront(&head, 4);
	SLTPrint(head);

	SLTPopFront(&head);
	SLTPrint(head);

        (3)尾插尾删

//尾插
void SLTPushFront(SLTNode** pphead,SLTDateType x)
{
    assert(pphead);
    SLTNode* newnode = BuySLTNode(x);
    //考虑极端空链表情况
    if(*pphead == NULL)
    {
        *pphead = newnode;
    }
    //循环遍历找到尾节点:
    else
    {
        SLTNode* tail = *pphead;
        //循环遍历找尾;
        while(tail->next)
        {
            tail = tail->next;
        }
        tail->next = newnode;
    }
  
}  
//尾删
void SLTPopBack(SLTNode** pphead)
{
    assert(pphead && *pphead);
    //找尾
    SLTNode* tail = *pphead;
    SLTNode* prev = tail;
    //这里得找到tail前一个节点才好删除;
    while(tail->next)
    {
        prev =tail;
        tail = tail->next;
    }
     prev->next = NULL;
     free(tail);
     tail = NULL;
}

         测试代码:

SLTNode* head = NULL;
SLTPushFront(&head,1);
SLTPushFront(&head, 2);
SLTPushFront(&head, 3);
SLTPushFront(&head, 4);
SLTPrint(head);

SLTPopFront(&head);
SLTPrint(head);

SLTPushBack(&head, 2);
SLTPrint(head);

SLTPushBack(&head, 3);
SLTPushBack(&head, 4);
SLTPushBack(&head, 5);
SLTPrint(head);

SLTPopBack(&head);
SLTPrint(head);

        (4)在任意位置的前面删除节点和删除任意位置节点

//在任意一个节点前插入新节点
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDateType x)
{
    assert(pphead && pos);
    if(pos == *pphead)
    {
        SLTPushFront(pphead);
    }
    else
    {
        SLTNode* prev = *pphead;
        SLTNode* newnode = SLTBuyNode(x);
        while(prev->next!=pos)
        {
           prev = prev->next;
        }
        newnode->next = pos;
        prev->next = newnode;
    }
}

        要找到那个节点可以和Find函数搭配使用;

       

//找到某位置
SLTNode* SLTFind(SLTNode* phead,SLTDateType x)
{
    SLTNode* cur = phead;
    while(cur)
    {
        //遍历链表数据域相同返回节点;
        if(cur->date == x)
        {
            return cur;
        }
        cur = cur->next;
    }
        //还没找到说明没有返回空指针;
        return NULL;
}

//删除任意位置的节点
void SLTErase(SLTNode** pphead,SLTNode* pos)
{    
    assert(pphead && pos);
    //与上面一样考虑特殊情况
    if(pos == *pphead)
    {
        SLTPopFront(pphead);
    }
    else
    {
        SLTNode* prev = pphead;
        while(prev->next)
        {
            prev = prev->next;
        }
        prev->next = pos->next;
        free(pos);
        pos = NULL;
    }
}

 测试代码

	SLTNode* head = NULL;
	SLTPushFront(&head,1);
	SLTPushFront(&head, 2);
	SLTPushFront(&head, 3);
	SLTPushFront(&head, 4);
	SLTPrint(head);

	SLTPopFront(&head);
	SLTPrint(head);

	SLTPushBack(&head, 2);
	SLTPrint(head);

	SLTPushBack(&head, 3);
	SLTPushBack(&head, 4);
	SLTPushBack(&head, 5);
	SLTPrint(head);

	SLTPopBack(&head);
	SLTPrint(head);

	SLTInsert(&head, SLTFind(head, 1), 0);
	SLTPrint(head);

	SLTErase(&head, SLTFind(head, 3));
	SLTPrint(head);

 

        (5)销毁链表      

        

//销毁链表
void SLTDestroy(SLTNode** pphead)
{
    SLTNode* cur = *pphead;
    SLTNode* Del = cur;
    while(cur)
    {    
        Del = cur;
        cur = cur->next;
        free(Del);
        //这里得特别注意顺序,要不然会删少;
    }
        //别忘了将头置为空
        *pphead = NULL;
}

 测试代码:
 

SLTNode* head = NULL;
SLTPushFront(&head,1);
SLTPushFront(&head, 2);
SLTPushFront(&head, 3);
SLTPushFront(&head, 4);
SLTPrint(head);

SLTPopFront(&head);
SLTPrint(head);

SLTPushBack(&head, 2);
SLTPrint(head);

SLTPushBack(&head, 3);
SLTPushBack(&head, 4);
SLTPushBack(&head, 5);
SLTPrint(head);

SLTPopBack(&head);
SLTPrint(head);

SLTInsert(&head, SLTFind(head, 1), 0);
SLTPrint(head);

SLTErase(&head, SLTFind(head, 3));
SLTPrint(head);

SLTDestroy(&head);
SLTPrint(head);

        好了单链表到此差不多完结了,现在我们来讲讲链表的分类

三、链表的分类

        链表是否循环,是否带头,是否双向,这几个 一共组成了八种链表类型

双向:一个节点内包含两个指针,它可以找到前一个节点和后一个节点;

循环:可以是尾节点指向头节点这样的循环,但也有指向其中某个节点而形成了环状的,例如:

带头:指的是链表的头节点数据域为空,这样可以避免很多讨论:

        我们把这个头叫做哨兵位; 

        举个列子:

/*void SLTPushBack(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	//考虑极端空链表情况
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	//循环遍历找到尾节点:
	else
	{
		SLTNode* tail = *pphead;
		//循环遍历找尾;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}

}*/
//这是上面尾插的代码,我们需要去讨论它是否为空,但如果它是一个带头的链表的话,因为总存在一个
//哨兵位,链表不会为空,就可以略去链表是否为空的情况,但实际上哨兵位没有数据没有意义,所以
//一般我们打印链表时从head->next开始打印;
	SLTNode* guard = SLTBuyNode(0);//这是哨兵位,这里数据无意义;
	//尾插
	SLTNode* newnode = SLTBuyNode(1);
	SLTNode* tail = guard;
	while (tail->next)
	{
		tail = tail->next;
	}
	tail->next = newnode;
	SLTPrint(guard->next);

        紧接着,我们来讲一个带头双向循环链表的实现;

四、带头双向循环链表

1.创建节点和初始化链表

//创造节点
LTNode* LTBuyNode(LTDateType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	assert(newnode);
	newnode->date = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
//初始化节点
//为什么要有一个初始化函数呢?
//单链表都没有~~
//原因:单链表没写哨兵位,而且没有循环,直接创造一个节点就可以,而这里它作为一个循环链表,一开始 
//我们head的prev和next都指向自己,方便设置而已;
LTNode* LTInit()
{
	LTNode* head = LTBuyNode(0);
	head->next = head;
	head->prev = head;
	return head;
}

2.头插和头删

//头插
void LTPushFront(LTNode* phead,LTDateType x)
{
	assert(phead);
	//传一级指针:与单链表不同,在双向循环链表中我们不用改变节点那个指针变量,改变的是节点内指针的信息;
	//具体可以看我画的图
    //这里要特别注意改变的顺序;
	LTNode* newnode = LTBuyNode(x);
	phead->next->prev = newnode;
	newnode->next = phead->next;
	phead->next = newnode;
}
//打印链表
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;//第一个是哨兵位从第二个开始开始遍历打印;
	printf("<=>head<=>");
	while (cur != phead)//作为一个循环链表它遍历完后会重新回到头节点;
	{
		printf("%d<=>", cur->date);
		cur = cur->next;
	}
	printf("\n");
}
         为什么使用一级指针?

//判断链表是否为空
bool LTEmpty(LTNode* phead)//使用bool要包含头文件<stdbool.h>
{
	if (phead->next == phead)//当头指向自己说明链表为空;
	{
		return true;
	}
	return false;
}
//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	//链表是否为空呢?写一个函数来判断;
	assert(!LTEmpty(phead));
	LTNode* Del = phead->next;
	phead->next = Del->next;
	Del->next->prev = phead;
	free(Del);
	Del = NULL;
}

        测试一下

	LTNode * head = LTInit();
	LTPushFront(head, 1);
	LTPushFront(head, 2);
	LTPushFront(head, 3);
	LTPushFront(head, 4);
	LTPrint(head);
	
	LTPopFront(head);
	LTPrint(head);

         

3.尾插和尾删

//尾插
void LTPushBack(LTNode* phead, LTDateType x)
{
	assert(phead);
	//找尾:单链表找尾是要遍历,但双向循环不用,因为哨兵位的prev指向的是最后一个节点
	LTNode* newnode = LTBuyNode(x);
	LTNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}
//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTNode* Del = phead->prev;
	phead->prev = Del->prev;
	Del->prev->next = phead;
	free(Del);
	Del = NULL;
}

        上测试~~ 

	LTNode * head = LTInit();
	LTPushFront(head, 1);
	LTPushFront(head, 2);
	LTPushFront(head, 3);
	LTPushFront(head, 4);
	LTPrint(head);
	
	LTPopFront(head);
	LTPrint(head);

	LTPushBack(head, 2);
	LTPrint(head);


	LTPopBack(head);
	LTPrint(head);

 

4.在任意位置的前面删除节点和删除任意位置节点

//找到某节点
LTNode* LTFind(LTNode* phead, LTDateType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->date == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
//任意位置之前插入
void LTInsert(LTNode* pos, LTDateType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	LTNode* prev = pos->prev;
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}
//删除任意位置
void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* prev = pos->prev;
	prev->next = pos->next;
	pos->next->prev = prev;
	free(pos);
	pos = NULL;
}

        这里是测试:

	LTNode * head = LTInit();
	LTPushFront(head, 1);
	LTPushFront(head, 2);
	LTPushFront(head, 3);
	LTPushFront(head, 4);
	LTPrint(head);
	
	LTPopFront(head);
	LTPrint(head);

	LTPushBack(head, 2);
	LTPrint(head);


	LTPopBack(head);
	LTPrint(head);

	LTInsert(LTFind(head, 2), 3);
	LTPrint(head);

	LTErase(LTFind(head, 2));
	LTPrint(head);

5.销毁链表

//销毁链表
void LTDestroy(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

五、结尾

        累死了,第一次写这么长的文章,真累人,肝了10个多小时,写完已经头昏眼花了,所以有错位欢迎评论区指责,十分感谢,过几天应该会出链表的oj题.....

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值