【数据结构&算法】07-链表技巧&参考源码


前言

  1. 指针或引用的含义
  2. 指针丢失和内存泄漏
  3. 哨兵简化实现难度
  4. 边界条件处理
  5. 多看代码多练

李柱明博客:https://i.cnblogs.com/posts/edit-done;postId=15487326

指针或引用的含义

指针和引用都是一个意思,都是存储所指对象的内存地址。

理解指针非常重要,对于后期使用指针、函数传参等等都发挥着理论指导作用。

指针:

  • 变量的地址即为该变量的指针 ,理解为地址(指针)指向了该变量,也就是通过地址(指针)可以找到该变量。
  • 如果一个变量专门用来存放另一个变量的地址(指针),则称它为指针变量
  • 如一个 32bit 系统中,指针的值只是 4 个 byte 的数据而已。

指针丢失和内存泄漏

指针丢失&内存泄漏例子:

  • 如单向链表,插入节点时,注意指针的更新:

  • // 错误示范
    p->next = x;  // 将 p 的 next 指针指向 x 结点;
    x->next = p->next;  // 将 x 的结点的 next 指针指向 b 结点;
    
  • 上述代码中一个错误点就是:p->netx 已经由原来的 b 更新为 x 了,到第二行代码时原意为 x 的下一个想指向 b 的,但是指针丢失了,导致 x->next 指向了 x 本身,链表后面的节点全部丢失。后面丢失的节点就是内存泄漏了。

  • 注意:

    • 实时留意指针的状态。
    • 在操作链表时,必要时可以备份节点。如在删除单向链表的某个节点时,备份下前一个节点。
    • 删除节点时,释放节点内存。若有对应的指针变量,记得赋空值。

注意野指针:

  • 指针变量创建时记得赋空值。
  • 释放内存时,记得赋空值。

哨兵简化实现难度

哨兵为了处理边界问题。如首节点。

哨兵不参与逻辑业务。

编程建议:链表尽量弄个链表句柄,方便管理。首节点的指针可以当做链表的句柄使用。

在链表中,第一个节点都需要特殊处理,如删除单向链表的第一个节点,就得把指向该链表的指针更新为第二个节点。

这样每次操作链表都需要检查是否是第一个节点,如果是还要特殊处理,显得繁琐。

所以我们固定一个首节点,实际数据的节点从第二个节点开始。这样就不用管链表是否为空。

首节点还可以记录链表的一些数据。

首节点的指针可以当做链表的句柄使用。

含首节点的链表交带头链表,相反叫不带头链表。

参考以下两段代码:

代码一:

// 在数组 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 很长的时候,执行效率更高的是代码二。

因为代码二的字符串检索是比代码一少了一个判断。

  • 代码一:O(2n)

  • 代码二:O(n)

    • 代码一比代码 二多了个字符串最大限度遍历限制。
    • 备份字符串最后一个字符,并把其改为遍历结束标志。
    • 完成遍历后再恢复该字符。

注意:上面代码只是给个例子,实际开发中,若不追求极致效率,为了代码可读性,不建议上面这个代码的实现。

边界条件处理

在处理链表时,先用大脑走一遍,主要是边界问题,如:

  • 链表为空时,代码是否正常工作?
  • 链表只有一个节点时,代码是否正常工作?
  • 链表有两个节点时,代码是否正常工作?
  • 处理链表第一个节点或最后一个节点时,代码是否正常工作?
posted @ 2021-11-05 09:53  李柱明  阅读(89)  评论(0编辑  收藏  举报