⭐顺序表和链表的这些细节你都注意到了吗?⭐

在这里插入图片描述


这几种数据结构完整的代码,都在我其他的博客里面,这篇博客主要的目的是分析其各自的一些细节,以及他们之间的一些对比的

顺序表的细节

🍎初始化

void SeqListinit(SLT * ps1)
{
	assert(ps1);                            //写这些断言的好处是万一错了他会报错,这儿还调试,下面的所有断言都是这个目的
	ps1->size = 0;
	ps1->capcaity =4;
	ps1->a = (SQDataType *)malloc(sizeof(SQDataType)*ps1->capcaity);
	if (ps1->a == NULL)
	{
		printf("创建失败\n");
	}
}

这里你可以选择先开辟4个内存空间,也可以选择不开辟,可以在SeqListPushBack里面开辟。

🍎头插和头删时要注意移动的顺序(防止元素的覆盖)
头插是从后往前,即后面的元素先往后移动一个,然后是前面的元素。

        int end = ps1->size-1;
		for (; end >= 0; end--)
		{
			ps1->a[end + 1] = ps1->a[end];
		}

头删是从前往后,即前面的元素先往前移动一个,然后是后面的元素。

	int i = 0;
	for (i = 1; i < ps1->size; i++)
	{
		ps1->a[i - 1] = ps1->a[i];
	}

🍎尾删时size直接减减就可以了,以免画蛇添足

void SeqListPopBack(SLT* ps1)
{
	assert(ps1);
	/*ps1->a[ps1->size - 1] = 0; */ //这一步反而是画蛇添足,因为万一我a数组的类型不是int型的或者我最后一个数正好就是0了,所以直接size--就行了
	ps1->size--;
}

🍎任意位置插入:注意pos是size_t的类型的,以及无符号数被当成有符号数时如何解决

void SeqListInsert(SLT* psl, size_t pos, SQDataType x)     //size_t的写法,这儿要注意当end为-1被当成无符号数会是一个很大的数,要想办法解决这么一个问题
{
	assert(psl);
	assert(pos <= psl->size && pos >= 0);
	SeqListCheckCapcaity(psl);

	/*int end = psl->size - 1;
	while (end >= (int)pos)
	{
	psl->a[end + 1] = psl->a[end];
	--end;
	}*/

	size_t end = psl->size;                        // 换种写法,巧妙的避开了这个问题
	while (end > pos)
	{
		psl->a[end] = psl->a[end - 1];
		--end;
	}

	psl->a[pos] = x;
	psl->size++;
}

如果这里你将end赋值为ps1->size-1就会出现无符号数被当成有符号数的那个问题,上面的代码就很巧妙的解决了这个问题

🍎 最后记住一定要释放开辟的内存,不然会内存泄漏的,下面几个数据结构也要注意,这里说下,下面就不再赘述了。

void SeqListDestory(SLT *ps1)
{
	assert(ps1);
	if (ps1->a != NULL)
	{
		free(ps1->a);
		ps1->a = NULL;
	}
}

单链表的细节

🥑 传参时,如果要改变头节点,就传二级指针,不需要改变头节点,就传一级指针。

//值传递,不需要对外面头结点进行改变
void SListPrint(SListNode* ps);
SListNode* SListFind(SListNode* phead, SLDataType x);
void SListInsertAfter(SListNode* pos, SLDataType x);
void SListEraseAfter(SListNode* pos);        
int SListSize(SListNode* ps);
bool SListEmpty(SListNode* ps);

//地址传递,要对外面头结点进行改变
void SListPopBack(SListNode** ps);
void SListPushBack(SListNode** ps, SLDataType x);
void SListPopFront(SListNode** ps);
void SListPushFront(SListNode** ps, SLDataType x);
void SListInsert(SListNode** pphead, SListNode* pos, SLDataType x);   //在某个节点之前插入
void SListErase(SListNode** ps, SListNode* pos);     //删除pos位置

🥑 单链表中有很多函数都需要分类导论,要考虑它只有一个节点或没有节点的极端情况

//单链表尾删
void SListPopBack(SListNode** ps)
{
	assert(ps);
	assert(*ps);
	SListNode* tail = *ps;
	SListNode* prev =*ps;
	//单链表一个节点的情况
	if ((*ps)->next == NULL)
	{
		free(*ps);
		*ps = NULL;
	}
	//单链表中有节点的情况
	else
	{
		while (tail->next)     //注意这儿和打印的地方不一样,这儿要这么写,具体情况要具体分析,自己举几个列子就知道了
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		/*tail = NULL;*/
		prev->next = NULL;
	}
}
//单链表尾插
void SListPushBack(SListNode** ps, SLDataType x)
{
	assert(ps);
	SListNode* tail = *ps;
	//没有节点的情况
	if (*ps == NULL)
	{
		*ps = SListBuyNode(x);
	}
	//有节点的情况
	else
	{
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = SListBuyNode(x);
	}
}

头删头插也同样要注意,因为这篇文章重点不是将代码的实现,所以这里就不传上代码,以免浪费篇幅。

🥑 注意写循环条件时,要先想好,我到底需不需要最后一个节点,不同的情况while里面的条件时不一样的。

SListNode* SListFind(SListNode* phead, SLDataType x)
{
	SListNode* cur = phead;
	while (cur)                  //这儿一定要写cur,不能写cur->next,不然最后一个节点是进不去的
	{
		if (cur->x == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}

	return NULL;
}

🥑 注意在不需要改变头节点时,万万不能随便解引用头节点以及连续写两个->next时,因为这种情况下很容易出现问题。




//在某个节点之前插入
void SListInsert(SListNode** ps, SListNode* pos, SLDataType x)
{
	assert(ps);
	SListNode* tail = *ps;
	SListNode* prev = *ps;
	//空节点的情况
	if (*ps == NULL)           //注意这儿千万不能写成tail,然后对tail进行操作
	{
		*ps = SListBuyNode(x);
	}
	//一个节点的情况
	else if ((tail)->next == NULL)
	{
		struct SListNode* newnode = SListBuyNode(x);
		*ps = newnode;
		newnode->next = tail;
	}
	//多个节点的情况
	else
	{
		while ((tail) != pos)
		{
			prev = tail;
			tail = tail->next;
		}
		/*prev->next = SListBuyNode(x);
		prev->next->next = tail;      */   //因为前面的SListBuyNode是你自己创建的一块空间,这块空间不与原链表完整的连接在一起,所以这句代码会出问题

		struct SListNode* newnode = SListBuyNode(x);
		newnode->next = tail;
		prev->next = newnode;
	}
}

双向带头循环链表的细节

🍆 注意这儿的带头,它的头和单链表的头理解有所不同,这儿的头只的是哨兵位,初始化好了之后是不能去改变它的,而单链表里面的头只的是第一个节点,我需要改变时是可以改变的。这也就是为什么这儿传参时大部分都传的是一级指针的原因。

void ListInit(struct ListNode** pphead);
void ListPrint(struct ListNode* phead);
void ListPushBack(struct ListNode* phead, ListDataType x);
struct ListNode* BuyNode(ListDataType x);
void ListPopBack(struct ListNode* phead);
void ListPushFront(struct ListNode* phead, ListDataType x);
void ListPopFront(struct ListNode* phead);
struct ListNode* ListFind(struct ListNode* phead, ListDataType x);
void ListInsert(struct ListNode* phead, struct ListNode* pos, ListDataType x);
void ListErase(struct ListNode* phead, struct ListNode* pos);
bool ListEmpty(struct ListNode* phead);
int ListSize(struct ListNode* phead);

🍆初始化时要注意:要先创建好那个空间,不然指针也没办法用呀;双向带头循环初始化时要自己指向自己

//初始化
void ListInit(struct ListNode** pphead)
{
	/*(*pphead)->data = 1;              //第一个节点的空间还没有创建了,所以不能这么写
	(*pphead)->pre = NULL;
	(*pphead)->next = NULL;
	(*pphead)->next = *pphead;
	(*pphead)->pre = *pphead;*/
	*pphead = BuyNode(-1);
	(*pphead)->next = *pphead;
	(*pphead)->pre = *pphead;
}

🍆 注意每个函数断言的写法,有时要这样断言assert(phead);这是为了保证头节点(哨兵位的节点)必须要有,不能为空,也有的是这样断言的assert(phead->next!=phead);这是为了保证链表中除了哨兵位外的节点必须要有节点存在。具体可以到我之前博客的代码中看。
🍆 注意在任意位置删除或插入时,一定要先把你要的那个位置找出来,还有就是Find和这两个函数最好分开来写。

    struct ListNode* pos = ListFind(phead, 3);   //ListInsert的复用,他也可以用来实现头插
	ListInsert(phead, pos, 4);
	ListPrint(phead);

栈的细节

栈可以由前面三种结构的任意一种来实现,所以栈的很多细节也就是前面三种数据结构的一些细节。

🍈 注意释放时的写法(用顺序表实现时)

void StackDestory(struct Stack* st)
{
	assert(st);
    while (st->top!=0)
{
	//st->a这个是一块连续的空间,释放时应该写
    //成free(st->a),应该他空间是连续的,像我
	//这种写法就是错误的
    free(st->a[st->top-1]);      
	st->top--;
}
if (st->a)
{
	free(st->a);
}
	st->a = NULL;
    st->top = 0;
	st->capacity = 0;
}

🍈 万一创建失败了,这种用临时变量过渡的写法比较好。

	//最好不要这样写,万一创建失败了,创建失败
		//就不能直接给st->a了,所以最好有个临时变量
		//来过渡一下
		st->a = (STDataType*)realloc(st->a, sizeof(STDataType)* 4);
		//细节方面,最好加上,防止创建失败
		if (st->a == NULL)
		{
			printf("realloc failed\n");
			exit(-1);
		}

		//较好的写法
		STDataType* temp = (STDataType*)realloc(st->a, sizeof(STDataType)* 4);
		if (temp == NULL)
		{
			printf("realloc failed\n");
			exit(-1);
		}
		else
		{
			st->a = temp;
		}

🍈 有些函数使用时,是要断言一下栈为空的情况的。

几者之间的对比

在这里插入图片描述

在初始化方面这几者也有所不同
单链表:
通过之前的文章,我们发现单链表并没有初始化这个接口,这是因为它的初始化其实在尾插或头插里面完成的。
在这里插入图片描述
顺序表和双向带头循环链表:
这两个链表是必须要初始化这个接口的:为什么顺序表要了?首先它必须要知道自己目前有几个数据吧,也必须要知道自己当前的容量是多少吧,不然你下面的函数怎么判断它是否要扩容了。那为什么双向带头循环链表要了?其实它是最必须要初始化的一个,因为这个链表必须要有一个哨兵位的头节点,而且它初始化时使自己指向了自己,因为形成了循环。

链接

这是我写的顺序表,单向链表,双向带头循环链表以及栈的实现的具体代码,这篇文章主要讲的是一些细节,想了解的更加详细的小伙伴可以去以下链接看看.

🤑双向带头循环链表实现的具体代码
栈的实现代码以及一个另人深思的问题😶
🙆单链表的实现
😃 顺序表的实现

  • 11
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一个数学不怎么好的程序员

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

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

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

打赏作者

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

抵扣说明:

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

余额充值