C语言数据结构易错知识点(2)

1.free报错时如何检查

首先free肯定是针对在堆区开辟的空间的,这个其实基本上都知道,也很少有人犯错。但很多时候我们的free操作还是会报错,如下图:

当这里出现报错时,很多人会下意识去看有没有free栈区空间,开辟堆空间的指针有没有被移动过。这些是直接错误导致的,但如果这些都没问题,那十有八九是在malloc后出现了越界访问,很多情况下越界访问是因为一些细节导致。首先看malloc的大小是否有误,特别是有没有正确使用sizeof;再调试看中间有没有对开辟空间以外的堆空间解引用,这两点注意了一般可以找出问题。

2.链表在内存中存储的方式

上一篇博客我分享了顺序表在两种创建方式下应当注意的问题,这篇博客会继续分析链表在两种创建方式下在内存中存储的方式和应当注意的事项。

先看一下下面的代码,new1和new2,分明用这两种方式创建的链表应当如何维护?

new1:一般情况下用这种办法创建链表,使用这种方式的话,我们在栈区创建的是一个指针,但此时这个指针相当于野指针,不能使用,要让它指向一片ListNode的空间,这个空间会在堆上开辟

以此类推,每个结点都是在malloc上开辟的,使用new1指针能够访问它们,在初始化时需要传二级指针(传址),因为new1的值指向了第一个结点,这个结点是找到后续节点的关键,在初始时new1是野指针,需要改变它存储的值,让它的值指向第一个结点,所以要用二级指针(指针的指针),而后续的接口就不需要二级指针,因为访问后续的结点只需要找到第一个节点即可,第一个节点可以通过new1存储的值找到,所以拿到new1的值就可以对后续结点实行操作,传值即可。如果第一个结点的地址变了,则仍然要传址(二级指针),原理同上。

new2:用这种方式创建链表,相当于在创建new2时创建了一个节点,然后这个结点的next指针指向堆区的后续的结点,在维护链表时,注意维护好new2结点,在初始化、创建或修改第一个结点new2时要传结构体指针,因为要修改结构体内部的变量的值,而后续接口的实现只需要传next的值即可,一般来说直接传结构体更方便

两种写法均可。如果链表含有哨兵位,可以考虑使用new2来创建链表。

3.栈和队列

栈(先进后出)和队列(先进先出)是相反的两种数据结构,栈多用顺序表实现,队列多用链表实现。其中,要清楚是在哪里进,又是在哪里出。栈是在顺序表的末尾实现进出操作,队列是在链表的末尾插入,在头部删除数据。按照这种方式可以更轻松地实现。

其中栈的实现和顺序表几乎相同,所以不再阐述。但队列的实现和链表的实现相比有一定的差别,

它使用了两个结构体,使得队列在内存中存储的逻辑稍微复杂了一点

因为队列要实现的是先进先出,需要我们实现一个尾插和头删的接口。之所以使用一个结构体,有三个原因:头删可以直接利用head来操作;尾插本可以通过时间复杂度为(n)的操作来实现,但太过麻烦,因此直接在链表的尾节点处存一个指针tail,这样时间复杂度为(1);同时,兼顾到维护上面两个指针,防止它们成为野指针(在free掉结点后防止tail指向被free掉结点的地址),所以加上size用于判定

清楚这一点后,我们要分析队列在内存中是怎样存储的,只有搞懂这个逻辑,写的时候才不会被指针搞得晕头转向:

和上面一样,Queue也有两种创建方式,这里只分析Queue new这种情况,另一种同理。

在主函数中创建了new这个结构体,里面包含了两个指针和一个整型变量,其中那两个指针是野指针,需要被处理,这个时候初始化直接传结构体指针,将里面的值改为0和NULL

②创建第一个结点:

使用malloc创建这个结点并修改里面的值,再修改head和tail存储的值,让它们都指向这个地址。注意,这个时候依然要传结构体指针,因为要修改结构体里的变量的值

后续如果要增加结点,直接创建新的节点并修改值,让原tail的next指向新tail,再移动tail,这个操作也要传结构体指针

其余操作同理,不再分析。不难看出,这种情况下直接创建结构体Queue更方便一些,如果创建的是Queue* new,则还要单独malloc一块区域用于存储Queue,指针多了也更易混淆。

4.循环队列

实现这种队列有一定的难度,主要是有很多细节需要注意。

循环队列的核心在于要正确区分空和满的状态并能访问队头和队尾的数据,使用链表和顺序表都可以,由于顺序表在下标访问上有优势,所以这里使用顺序表来分享思路。

①区分空和满(假溢出问题)

区分空和满可以用size来解决,这里讨论的是不引入新的变量的办法

a.假设我们需要两个下标head和tail,tail指向末尾元素的下一个下标,空的时候head == tail

但这个时候会出现一个问题,由于是循环队列,当满的时候head == tail,因为4(值而不是下标)的下一个下标是0

b.如果让tail在空的时候值为-1,其余时候指向末尾元素的下一个下标也不能区分,并且在空的时候

也可能存在head == tail,如下图:

当删除3后,为空,但head == tail

c.那如果让tail在空的时候值为-1,其余时候指向末尾元素的下标,也不可行

当push了1,2,3,4并pop1,2,3后head == tail而且这个时候队列既不为空也不为满,更没有办法判定了。

类似的假设有很多,但都没办法区分空和满的问题,这也叫假溢出问题。假溢出问题的解决方法是:在指定队列大小是k的情况下,开辟k + 1个空间,多余的空间不存储数据但可以让下标指向它,head指向首元素,tail指向尾元素的下一个下标下面来看看效果:

a.队列为空时head == tail

b.队列满的时候(tail + 1) % (k + 1) == head

如果我们pop几个元素,在增加几个元素,发现空位是随机的,也满足上述规律:

这个时候就满了。

如果要访问队首的元素,直接访问head下标对应的元素即可,如果要访问队尾,则考虑到下标循环的情况,用(tail - 1 + k + 1) % (k + 1)即可访问。

假溢出比较常见,这种方法要掌握。

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值