链表(二)如何轻松写出正确的链表代码

链表(二)如何轻松写出正确的链表代码


01、技巧一:理解指针或引用的含义

有些语言有指针的概念,比如c语言;有些没有指针,用的是“引用”,比如java、py;隐私都一样,都是存储所指对象的内存地址;

指针 & 引用:

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量

例如:p->next=p->next->next

这行表示:p 结点的 next 指针存储了 p结点的下下一个结点的内存地址。


02、技巧二:警惕指针丢失和内存泄漏

指针一般都是这样弄丢的?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7GnauHA-1607353284736)(G:\笔记\算法\image\指针丢失.png)]

如图:我们希望在结点 a 和相邻的结点 b 之间插入结点 x ,假设当前指针 p 指向结点 a 。

    p->next = x;  // 将 p 的 next 指针指向 x 结点;
    x->next = p->next;  // 将 x 的结点的 next 指针指向 b 结点;

在这。p->next 指针在完成第一步操作之后,已经不再指向结点 b 了,而是指向结点 x 。而第2行代码相当于将 x 赋值给 x->next ,这样就会自己指向自己,导致整个链表被断成两半;

插入结点时,一定要注意操作的顺序。要先将结点 x 的next 指针指向结点 b ,再把结点 a 的next 指针指向结点 x;

  x->next=p->next;//将x的结点的next指针指向p的next结点(b结点)
  p-next=x// 将 p 的 next 指针指向 x 结点;

同理,删除链表结点时,也一定要记得手动释放内存空间,否则也会出现内存泄漏的问题。但是对于java这种虚拟机自动管理内存的语言来说,不需要考虑这种情况;


03、技巧三:利用哨兵简化实现难度

回顾单链表的插入和删除。

如果我们在结点p后面插入一个新的结点:

  new_node->next=p->next;
  p->next=new_down;

但是,当要向一个空链表中插入第一个结点,这样的逻辑就不能用了。需要处理,其中head表示链表的头结点:

  if(head==null){
  	head=new_node;
  }

对于单链表的插入操作,第一个结点和其他结点的插入逻辑是不一样的。

如果要删除结点p的后继结点,只需要:

  p->next=p->next->next;

但是,如果要删除链表中的最后一个结点,跟插入类似,也需要特殊处理:

   if(head->next == null){
   		head=null;
   }

从上案例来看,**针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。**但是这样实现起来很繁琐,也可能回考虑不全而出错。

如何解决?

哨兵!!!

哨兵定义:生活中哨兵解决的是国家之间边界的问题。同理,在这里哨兵也是解决 边界问题的,不直接参与业务逻辑

上面案例中有一个 head=null 表示链表中没有结点了。其中head表示头结点指针,指向链表中的第一个结点。

如果我们引入哨兵站点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫做 带头链表。相反的叫做 不带头链表

带头链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DHnQMgd3-1607353284738)(G:\笔记\算法\image\带头链表.png)]

在此类型链表中,哨兵站点是不存储数据的。因为哨兵站点一直存在,所以进行插入、删除操作就都可以统一为相同的代码逻辑了;

这种用法在很多地方都有用到,比如插入排序、归并排序、动态规划等。

代码一:

        // 在数组 a 中,查找 key,返回 key 所在的位置
    // 其中,n 表示数组 a 的长度
    int find(char* a, int n, char key) {
      // 边界条件处理,如果 a 为空,或者 n<=0,说明数组中没有数据,就不用 while 循环比较了
      if(a == null || n <= 0) {
        return -1;
      }

      int i = 0;
      // 这里有两个比较操作:i<n 和 a[i]==key.
      while (i < n) {
        if (a[i] == key) {
          return i;
        }
        ++i;
      }

      return -1;
    }

代码二:

    // 在数组 a 中,查找 key,返回 key 所在的位置
    // 其中,n 表示数组 a 的长度
    // 我举 2 个例子,你可以拿例子走一下代码
    // a = {4, 2, 3, 5, 9, 6}  n=6 key = 7
    // a = {4, 2, 3, 5, 9, 6}  n=6 key = 6
    int find(char* a, int n, char key) {
      if(a == null || n <= 0) {
        return -1;
      }

      // 这里因为要将 a[n-1] 的值替换成 key,所以要特殊处理这个值
      if (a[n-1] == key) {
        return n-1;
      }

      // 把 a[n-1] 的值临时保存在变量 tmp 中,以便之后恢复。tmp=6。
      // 之所以这样做的目的是:希望 find() 代码不要改变 a 数组中的内容
      char tmp = a[n-1];
      // 把 key 的值放到 a[n-1] 中,此时 a = {4, 2, 3, 5, 9, 7}
      a[n-1] = key;

      int i = 0;
      // while 循环比起代码一,少了 i<n 这个比较操作
      while (a[i] != key) {
        ++i;
      }

      // 恢复 a[n-1] 原来的值, 此时 a= {4, 2, 3, 5, 9, 6}
      a[n-1] = tmp;

      if (i == n-1) {
        // 如果 i == n-1 说明,在 0...n-2 之间都没有 key,所以返回 -1
        return -1;
      } else {
        // 否则,返回 i,就是等于 key 值的元素的下标
        return i;
      }
    }

对比两段代码,在字符串 a 很长的时候,比如几万、几十万,代码二运行得更快点,因为两段代码中执行次数最多就是 while 循环那一部分。第二段代码中,我们通过一个哨兵 a[n-1] = key,成功省掉了一个比较语句 i<n,不要小看这一条语句,当累积执行万次、几十万次时,累积的时间就很明显了。


04、技巧四:重点留意边界条件处理

一般情况下,代码在一些边界或者异常情况下,做容易产生Bug。

检查链表代码是否正确的边界条件:

  • 如果链表为空,代码是否能正常工作?
  • 如果链表只包含一个结点时,代码是否能正常工作?
  • 如果链表只包含两个结点时,代码是否能正常工作?
  • 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

05、技巧五:举例画图,辅助思考

对于稍微复杂的链表,用 举例法画图法

比如往单链表中插入一个数据这样一个操作:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-izgWsZEE-1607353284739)(G:\笔记\算法\image\画图法.png)]


06、技巧六:多写多练,没有捷径

五个常见的链表操作:

  • 单链表反转
  • 链表中环的检测
  • 两个有序链表合并
  • 删除链表倒数第n个结点
  • 求链表的中间结点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值