• 05 链表(下):如何轻松写出正确的链表代码?


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

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

    常见链表代码

    p->next=q

    这行代码是说,p结点的next指针存储了q结点的内存地址。

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

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

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

    单链表的插入操作

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

    在上面这段代码中,p->next 指针在完成第一步操作之后,已经不再指向结点b了,而是指向结点x。第二行代码相当于将x赋值给x->next,自己指向自己。因此,整个链表也就断成了两半,从结点b往后的所有结点都无法访问到了。
    x->next=p->next; 而p->next=x; 所以 x->next=p->next;等价于x->next=x; 所以指针断裂,x节点之后的节点丢失了。
    上面代码只要将第1行和第2行代码颠倒下就OK了。

    • 插入结点时,一定要注意操作的顺序
    • 删除链表结点时,也一定要记得手动释放内存空间

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

    单链表的插入和删除操作

    在结点p后面插入一个新的结点

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

    空链表中插入第一个结点

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

    删除结点p的后继结点

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

    删除链表中的最后一个结点

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

    针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。

    哨兵,解决的是国家之间的边界问题。同理,这里说的哨兵也是解决“边界问题”的,不直接参与业务逻辑。

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

    哨兵结点是不存储数据的。因为哨兵结点一直存在,所有插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑了。
    这种利用哨兵简化编程难度的技巧,在很多代码实现中都有用到,比如插入排序、归并排序、动态规划等。

    哨兵实现代码

    代码一

    // 在数组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[n-1] = key,成功省掉了一个比较语句 i<n,因此,当字符串a很长的时候,比如几万、几十万次时,第二段代码运行的更快(因为两段代码中执行次数最多的是while循环那部分)。

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

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

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

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

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

    5个常见的链表操作

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

    练习题LeetCode对应编号:206,141,21,19,876

  • 相关阅读:
    HTML5的结构
    关于本Blog
    Luogu2568 GCD
    CH5102 Mobile Service
    Luogu3146 [USACO16OPEN]248
    Mobile Service
    23、Echarts拓扑图、D3拓扑图
    22、startAngle: 85-88、ECharts之仪表盘、仪表盘镂空且导入外来图片、品牌车、不等距折线图、图片右侧空白再出图片、心电图、多数据单环、单数据单环、单数据双环、饼图效果、经纬度、10年经济指标(无局部放大)
    21、angular1之分页组件(包含在复杂弹窗组件里,分页组件包含勾选、过滤、拖拽、翻页记忆、请求服务、转圈服务、简单弹窗组件、插槽ng-transclude)
    19、angular1全局方法、五种服务类型、过滤、重要指令(ng-)、单选框|复选框|下拉框三者联合案例展示、下拉表格嵌套、子组件向父组件传值、directive自定义标签、获取不到新value、[].forEach|$.each|angular.forEach用法示例、undefined+1、ui.router路由模块、ui.router实际执行步骤、jqLite的API参考、前端路由、类
  • 原文地址:https://www.cnblogs.com/zyl1994/p/14814815.html
Copyright © 2020-2023  润新知