zoukankan      html  css  js  c++  java
  • 【数据结构&算法】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)

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

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

    边界条件处理

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

    • 链表为空时,代码是否正常工作?
    • 链表只有一个节点时,代码是否正常工作?
    • 链表有两个节点时,代码是否正常工作?
    • 处理链表第一个节点或最后一个节点时,代码是否正常工作?
  • 相关阅读:
    UVA 10618 Tango Tango Insurrection
    UVA 10118 Free Candies
    HDU 1024 Max Sum Plus Plus
    POJ 1984 Navigation Nightmare
    CODEVS 3546 矩阵链乘法
    UVA 1625 Color Length
    UVA 1347 Tour
    UVA 437 The Tower of Babylon
    UVA 1622 Robot
    UVA127-"Accordian" Patience(模拟)
  • 原文地址:https://www.cnblogs.com/lizhuming/p/15487326.html
Copyright © 2011-2022 走看看