剑指 offer —— 刷题笔记

本文分享了编程中重要的习惯,如清晰编码、命名规范、单元测试、调试技巧、边界条件考虑及代码鲁棒性。还讲述了刷题过程中遇到的常见问题,如头文件用法、const char初始化、命令行参数理解,以及单例模式、数组操作、链表遍历和二叉树重构的技巧。

一、写在前面

书中正题之前介绍了很多关于面试以及写代码的一些小技巧。面试技巧可以临面试前再现场模拟,但是关于写代码的现在就需要开始注意了,以下列出几个编程习惯的要点。

  • 1、思考清楚再开始编码,思考的过程可以通过自己例举一些测试用例,通过实际用例的推导,来模拟整个实现过程,进而找出隐藏的规律,然后再去思考代码的具体实现。
  • 2、良好的代码命名和缩进对齐习惯
  • 3、能够进行单元测试
  • 4、debug 的调试功底,能够熟练地设置断点,单步跟踪,查看内存,分析调用栈,快速的发现问题并解决问题。
  • 5、要学会自己思考一些边界条件,特殊用例,也就是自己需要考虑到尽可能全面的测试用例等,在OJ上或许不重要,但是在面试时,这些才是考察你能力的关键。关注代码的鲁棒性。
  • 6、学会优化代码,从时间复杂度和空间复杂的的角度来考虑,这就需要了解每一种数据结构的优缺点。

二、刷题记录

第1题 赋值运算符重载

问题1 关于头文件

刚开始就遇到一个小问题,可真是bug,混淆了 #include"xx.h" 和 #include<xx.h> 的用法

// 表示直接从编译器自带的函数库中寻找文件,编译器从标准库路径开始搜索.xxh
#include<xx.h> 

// 表示先从自定义的文件中找 ,如果找不到在从函数库中寻找文件,编译器从用户的工作路径开始搜索 xx.h
#include"xx.h" 

/*
如果我们通过<>的方式引用自己编写的头文件,必然会出现无法找到与源文件的问题,因为我们的文件放在了用户目录下,上面的解决办法本质上是通过将会用户目录追加到编译器搜索范围内,其实通过将<>换成" "就可以解决问题了。
*/
问题2 coust 初始化

const char 类型的值不能初始化char ,这个是新的标准中的规定,不能把指针指向一串常量

解决方法:

1、 const char* = “Hello world”;

2、 char temp[] = “Hello world”;
char* = temp;

3、 右键project -> 属性 -> C/C++ -> 语言 -> 符合模式:否

问题3 argc, argv 是什么?

会常常看到mian函数这么写: int main(int argc,char* argv[]) { }

argc,argv可以使程序接受命令行参数,更方便的IO

argc 是 argument count的缩写,表示传入main函数的参数个数;

argv 是 argument vector的缩写,表示传入main函数的参数序列或指针,并且第一个参数argv[0]一定是程序的名称,并且包含了程序所在的完整路径,所以确切的说需要我们输入的main函数的参数个数应该是argc-1个

conclusion
  • 1、为了可以进行连续赋值运算,则返回值类型必须要能够接收一个新的对象。
    • 1.1 返回值类型是否设定为该类型的引用,也就是用 by reference的方式。
    • 1.2 函数的结束语句是否为 return *this;
  • 2、是否把形参列表的参数类型声明为常量引用
    • 2.1 如果不是引用而是一个实例的话,那么从形参到实参会调用一次复制构造函数,效率降低
    • 2.2 因为赋值不改变传入实参的状态,因此要为常量const
  • 3、是否释放实例自身已有的内存,如果
  • 4、是否判断传入的参数和当前的实例(*this)是不是同一个实例。

对于程序安全性的理解,不懂哇,看书也没有懂 ~

第2题 实现单例模式Singleton

问题1 class和struct区别
  • 如果没有标明成员函数或者成员变量的访问权限级别,
  • 在struct中默认是public,class中默认是private

第3题 数组中的重复数字

本题主要考虑两种解法:

方法一:哈希表
  • 时间复杂度 O(n) : 遍历数组使用 O(n) ,HashSet 添加与查找元素皆为 O(1) 。
  • 空间复杂度 O(n) : 遍历数组使用 O(n)
  • 才想到一个问题就是 unordered_map 初始化的时候默认是0 啊
方法二:原地排序
  • 时间复杂度 O(n) :

  • 空间复杂度 O(1) :

  • 注意这个方法要用 while 循环来进行遍历,因为下标是跳来跳去的昂~

  • 点睛:如果相等,就继续下一个下标的判断,如果不相等,交换元素之后,仍然对本下标位置的值进行判断

conclusion
  • sort 函数的时间复杂度为 O(n*log2n),因此不要用先排序再邻位比较的方法了
  • unordered_map 初始化的时候默认是0
  • 数组下标的类型为:size_t

第4题 二维数组中的查找

conclusion
  • 本题的规律可以简单总结为:找到比最大的大,或者是比最小的小,然后将行或者列直接删掉。
  • 在32位操作系统上,一个指针就是一个 int ,所占空间大小为4个字节
  • 在64位操作系统上,一个指针也是一个 int ,所占空间大小为8个字节

第5题 替换空格找

字符数组与字符串的关系

第6题 从尾到头打印链表

递归在本质上就是一种栈的结构,如果可以用栈来实现,自然而然就想到可以用递归来实现。
递归代码虽然看起来简洁,但是当链表非常长的时候,就会导致函数调用的层级很深,从而有可能导致函数调用栈溢出。

因此用栈基于循环实现的代码的鲁棒性要好一些。

  • 方法一:用栈实现先进后出

  • 方法二:递归

    • 递归的思想:要实现反过来输出链表,我们每访问到一个结点时,先递归的输出他后面的结点,
    • 再输出自身结点,这样链表的输出结果就反过来了。

第7题 重建二叉树

下面这些可以忽略不看,是在跟着 Carl老师 系统学习之前自己总结的,一个月过去了,感觉写了这么多也舍不得删掉,就暂且先留着吧。感恩遇到了 Carl老师 让我的学习快速的进步~ 感恩,敬畏,加油~

对于任意一棵树,前序遍历形式为:[根节点,[左子树的前序遍历结果],[右子树的前序遍历结果]]
对于任意一棵树,中序遍历形式为:[[左子树的前序遍历结果],根节点,[右子树的前序遍历结果]]

因此我们只要在中序遍历中定位到根节点,那么就可以分别知道左子树和右子树的节点数目。
由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的
结果中,对上述形式中的所有左右括号进行定位。

在中序遍历对根节点进行定位时,有两种思路:
一种直接的方法就是直接扫描整个中序遍历的结果,并找出根节点,但是这样做的时间复杂度较高,每一次的定位都需要遍历一次。
一种简单的方法是使用哈希表来帮助我们快速的定位根节点。对于哈希映射中的每个健值对,key健 表示一个元素,也就是节点的值,value值 表示其在中序遍历中的出现位置。在构造二叉树之前,可以先对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射,在此后构造二叉树的过程中,我们就只需要时间复杂度为O(1)的时间对根节点进行定位了

课本上的大概编程思路:

1、因为题目给的是根据前序遍历和中序遍历,来重建二叉树,并返回头结点,看的案例一般都是两个函数,一个创建的函数,还有一个是调用创建函数,得到最终的结果。

  • 为什么要两个函数呢?
  • 因为两个函数的传入参数不一样。Construct函数的参数是为了符合题意,有两个形参,分别是前序遍历的指针和中序遍历的指针。ConstructCore函数的形参有四个,前序遍历和中序遍历的第一个节点的地址和最后一个节点的地址,这样便于去寻址。注意Construct函数传入的形参也是地址,相当于第一个节点的地址。

2、对于调用创建函数的函数,这个很简单,只有两步,第一步是判断前序遍历和中序遍历是否为空指针,以及长度是否小于0,三者有一个不满足,则返回NULL;第二步是返回第一个函数的调用结果。

3、在ConstructCore这个函数中,假定传入的是四个参数,即只有两个遍历的首下标。

  • 第一步:前序遍历的第一个数字就是根节点的值,因此创建一个根节点,把这个data存进去,然后把左右子树初始化为NULL。

  • 第二步:判断前序遍历的第一个节点和最后一个节点是否相等:

    • 如果不相等,跳过继续往下执行;
    • 如果相等,再判断中序遍历的第一个节点和最后一个节点是否相等 && 前序遍历的第一个节点的值与中序遍历的第一个节点的值是否相等,
      • 如果都相等,说明遍历结束,return root;
      • 否则就说明是非法输入;
  • 第三步:在中序遍历中找根节点,怎么找呢?

    • 定义一个临时的跟节点指针rootInorder,初始化为前序遍历的第一个节点,用while循
    • 环,找到满足rootInorder小于等于前序遍历的最后一个节点 且 rootInorder对应的
    • 元素与第一步中的根节点的值不等,rootInorder指针依次后移。因为如果rootInorder
    • 对应的元素等于根节点的值,说明在中序遍历中找到了根节点。
  • 第四步:判断找到的这个临时节点是否等于前序遍历中的最后一个节点 且 rootInorder对应的元素与第一步中的根节点的值不等,如果满足,则说明是非法输入。

  • 第五步:在中序遍历中计算左子树长度,注意是一个int变量:

    • int leftLength = rootInorder - startInorder;
  • 在前序遍历中计算左子树的最后一个节点,注意这里是一个指针:

    • int* leftPreorderEnd = startPreorder + leftLength;
  • 第六步:构建左子树。

  • 首先先判断计算出的左子树长度是否大于0,若大于0开始构建:

  • 构建左子树的思想就是递归:

  • 构建左子树,是根据前序遍历的左子树与中序遍历的左子树相结合:

    • 到这里才有root->m_pLeft=ConstructCore
    • 括号里四个参数分别是:
      • 前序遍历中左子树的第一个节点地址
      • 前序遍历中左子树的最后一个节点地址
      • 中序遍历中左子树的第一个节点地址
      • 中序遍历中左子树的最后一个节点地址
  • 第七步:构建右子树。

  • 首先判断左子树的长度是否小于前序遍历中最后一个节点指针域最后一个节点指针的差。

  • 构建右子树的思想也是递归:

  • 构建右子树,是根据前序遍历的右子树与中序遍历的右子树相结合:

    • root->m_pRigth= ConstructCore
      • 前序遍历中右子树的第一个节点地址
      • 前序遍历中右子树的最后一个节点地址
      • 中序遍历中右子树的第一个节点地址
      • 中序遍历中右子树的最后一个节点地址
  • 第八步:return root;

LeetCode参考答案的思路:

1、同样是创建了两个函数,这一点与课本上类似,buildTree函数调用myBuildTree函数来得到最
终的结果。这里有几点跟课本上思路不一样的,以下做说明:

  • 1)设置了一个私有变量,unordered_map<int, int> index;
  • 2)课本中在中序遍历中寻找根节点比较麻烦,通过while循环,设置了很多的判断条件,这
  • 里使用了哈希映射。在buildTree函数中,先遍历一次中序遍历,将其存储到一个哈希表组
  • 成的容器中,unordered_map<int, int> index;这样在后续取对应值的过程中就无需
  • 每次都判断了,节省了时间。
  • 3)课本上前序遍历和中序遍历的结果是用指向int类型的指针的数组来存储的,这里是用
  • vector容器存储,因为存储要满足动态存储和易于取出
  • 这里函数的参数不是指针了,前序遍历和中序遍历都是用vector数组存储的,但是参数
  • 也不能直接使用变量名,而是使用了引用。
    • 为什么要用引用呢?
    • 因为在递归的过程中,相当于是改变了前序遍历和中序遍历,不用引用的话,没法递归吖
  • 4)多了一个定义:int n = preorder.size();
  • 因为要使用下标作为接下来递归的参数,所以先定义出来尾下标。

2、myBuildTree的参数有6个,也不再用指针了,有两个是前序遍历和中序遍历的引用,还有四个是
首下标和尾下标,下标就是int类型了,而课本上用的是指针,也简化了很多。

3、介绍myBuildTree的步骤:

  • 第一步:判断前序遍历的左下标是否大于右下标,如果左大于右,则说明输入非法,返回nullptr.
  • 第二步:定义临时变量,第一个是前序遍历中的根节点(这个可以直接确定,就是第一个节点)
    第二个是中序遍历中的根节点(就是中序遍历中个前序遍历的根节点值相
    同的节点),这个是在一个前提下,这棵树中的节点值互不相等才成立,
    题中给了这个条件。
  • 第三步:创建根节点,并传入前序遍历的根节点进行初始化。
  • 第四步:根据中序遍历得到左子树中的节点数目:inorder_root - inorder_left;
  • 第五步:递归的构造左子树,并连接到根节点:root->left = myBuildTree
    • 参数一:前序遍历,
    • 参数二:中序遍历
    • 参数三:前序遍历首下标+1
    • 参数四:前序遍历首下标+左子树节点数目
    • 参数五:中序遍历首下标
    • 参数六:中序遍历根节点下标-1
      *第六步:递归的构造右子树,并连接到根节点:root->right = myBuildTree
    • 参数一:前序遍历
    • 参数二:中序遍历
    • 参数三:前序遍历下标+左子树节点数目+1
    • 参数四:前序遍历尾节点下标
    • 参数五:中序遍历根节点下标+1
    • 参数六:中序遍历尾结点
      *第七步:return root;

第9题 两个栈实现队列

操作系统的每个线程创建一个栈,用来存储调用来调用时各个函数的参数、返回地址、以及临时变量等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值