数据结构与算法之美学习笔记:第七讲

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

一、指针和引用有什么关系

二、代码实现

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

一、指针是如何弄丢的呢?

我拿单链表的插入操做为例来给你分析一下node

如图所示,咱们但愿在结点a和相邻的结点b之间插入结点x,假设当前指针p指向结点a。若是咱们将代码实现变成下面这个样子,就会发⽣指针丢失和内存泄露。python

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

二、插入结点时、必定要注意操做的顺序

三、删除链表结点时、必定要手动释放内存空间

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

一、发现问题

一、在结点P后面插入一个新的结点

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

二、向一个空链表中插入第一个结点

刚刚的逻辑就不能用了、须要进行下面这样的特殊处理、其中head表示链表的头结点、对于单链表的插入操做,第一个结点和其余结点的插入逻辑是不同面试

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

三、单链表的结点删除操做

若是要删除结点P的后继结点,咱们只须要一行代码就能够搞定编程

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

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

跟插入相似数组

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

从前面的一步一步分析,咱们能够看出,针对链表的插入、删除操做,须要对插入第一个结点和删除最后一个结点的状况进行特殊处理。这样代码实现起来就会很繁琐,不简洁,并且也容易由于考虑不全而出错。如何来解决这个问题呢?性能

二、解决问题

一、什么是哨兵

二、带头的链表和不带头的链表

我画了一个带头链表,你能够发现,哨兵结点是不存储数据的。由于哨兵结点一直存在,因此插入第一个结点和插入其余结点,删除最后一个结点和删除其余结点,均可以统一为相同的代码实现逻辑了。spa

 

三、哨兵简化了编程的难度

我再举一个很是简单的例子。代码我是用C语言语实现的,不涉及语言方面的高级语法、很容易看懂,你能够类比到你熟悉的语。3d

代码一指针

// 在数组 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,不要小看这一条语句,当累积执行万次、几十万次时,累积的时间就很明显了。

固然,这只是为了举例说明哨兵的做用,你写代码的时候千万不要写第⼆段那样的代码,由于可读性太差了。大部分状况下,咱们并不须要如此追求极致的性能。

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

一、咱们常常用来检查链表代码是否正确的边界条件有这样几个

二、针对不一样的场景

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

你能够找个个具体的例子,把它画在纸上,释放一些脑容量,留更多的给逻辑思考,这样就会感受到思路清晰不少。好比往单链表中插入一个数据这样一个操做,我通常都是把各类状况都举一个例子,画
出插入前和插入后的链表变化,如图所示:

看图写代码,是否是就简单多啦,并且咱们写完代码以后,也能够举几个例子、画在纸上,照着代码走一遍,很容易就能发现代码中的Bug

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

若是你已经理解并掌握了我前面所讲的方法法,可是手写链表代码仍是会出现各类各样的错误,也不要着急。由于我最开始学的时候,这种情况也持续了一段时间。

如今我写这些代码,简直就和“玩儿”同样,其实也没有什么技巧,就是把常见的链表操做都本身多写几遍,出问题就一点一点调试,熟能生巧!

因此,我精选了5个常⻅的链表操做。你只要把这几个操做都能写熟练,不熟就多写几遍,我保证你以后不再会惧怕写链表代码。

  1. 单链表反转
  2. 链表中环的检测
  3. 两个有序的链表合并
  4. 删除链表倒数第n个结点
  5. 求链表的中间结点


我以为写链表代码是最考验逻辑思惟能力的,由于链表代码导出都是指针的操做、边界条件的处理,稍有不慎就容易产生Bug链表代码写的好坏,能够看出一我的写代码是否够细心,

考虑问题是否全面、思惟是否缜密、因此,这也是不少面试官喜欢让人手写链表代码的缘由,因此,这一节讲到的东西,你必定要本身写代码实现一下,才有效果

相关文章
相关标签/搜索