面试1——剑指Offer笔记(C++)

源码

1 基础知识

1.1 编程语言

四个类型转换关键字

参考
1) static_cast
底层无const的一般静态转换,基本类型 / 具有继承关系的类型

//基本类型转换
int i = 0;
double d = static_cast<double>(i);  //相当于double d = double(i);
//转换继承类的对象为基类对象
class Base{};
class Derived: public Base{};
Derived d;
Base b = static_cast<Base>(d);  //相当于Base(d)

2) dynamic_cast
动态转换,派生类指针(引用)转换为基类指针(引用) / 没有继承关系但被转换类有虚函数

class Base{};
class Derived: public Base{};
//派生类指针转换为基类指针
Derived *pd = new Derived;
Base *pb = dynamic_cast<Base>(pd);
//检测
if(!pb){
  std::cout << "Failed Cast!";
}
//没有继承关系,但被转换类有虚函数
class A{
    virtual ~A();
};
class B{};
A *pa = new A;
B *pb = dynamic_cast<B>(pa);

3) const_cast
只改变const(常量)属性,不改变类型

const int *pci = 0;
//将常量转换为非常量
int *pk = const_cast<int*>(pci);  //相当于int *pk = int*(pci);
const A* pca = new A;
A *pa = const_cast<A*>(pca); //相当于A *pa = A*(pca);

4) reinterpret
重新解释、指针类型转换

//基本类型指针的类型转换
double d = 9.2;
double *pd = &d;
int *pi = reinterpret_cast<int*>(pd); //相当于int *pi = (int*)pd;
//不相关的类的指针类型转换
class A{};
class B{};
A *pa = new A;
B *pb = reinterpret_cast<B*>(pa); //相当于B *pb = (B*)pa;
//指针转换为整数
int *pi = 0;
long L = reinterpret_cast<long>(pi);  //相当于long L = (long)pi;

sizeof

1) 空类型使用sizeof,计算该类型实例的大小,使用实例,有大小,大小由编译器决定,一般是1
2) 有构造和析构函数的类型,一般也是1,实例调用这俩函数只需知道函数地址,函数地址与类型有关,与实例无关
3) 有虚函数的类型,大小为一个指针的大小(32/64->4/8),编译器会在实例中添加指向虚函数表的指针

复制构造函数

1) C++标准不允许复制构造函数进行值传递(递归溢出),允许常量引用

面试题1 赋值运算符函数

1) 返回引用(连续赋值),传入常量引用(高效、保护),释放已有内存(泄露),判断同一个实例
2) 异常安全
先释放内存还是先申请内存:内存不足的情况下,要保证原数据不变(不被提前释放),创建临时实例,构造函数new,new出来就交换,new不出来就抛出异常bad_alloc

1.2 数据结构

数组的效率

1) 静态数组:高时间效率,低空间效率
2) 动态数组:扩容操作增加时间复杂度(STL,vector)
3) 数组作为函数参数传递时,数组名将退化成同类型的指针

面试题3 数组中的重复数字

1) 题1找出重复数字,思路:把数字放到对应的下标的位置上,当发现对应位置已有该数字,则找到重复数字,对应空间O(1),时间O(n)
2) 题2不修改数组找出重复数字,思路:二分法,种类二分,确定每一段内的种类出现的次数,是否为该段的种类数。
3) 明确要求时间还是要求空间效率

面试题4 二维数组查找

1) 规律,缩小查找范围

常量字符串的地址

1) 常量字符串初始化字符数组,不同数组的地址不同,原因是不同字符数组已分配了不同位置的内存以拷贝常量字符串 P49
2) 常量字符串初始化字符指针,不同指针指向的地址相同,原因是无需重新分配内存,直接指向常量在内存中的地址

面试题5 替换空格

1) 一次移动到位,O(n),(此题不用自己扩容)
2) 考虑内存覆盖
3) 从尾部到头部,依次填入排序值

链表

1) 链表是考察重点
2) 为什么是传入指向指针的指针
要修改的是指针的内容(pHead,也就是修改一个地址),而不是修改指针指向的内容,因此传入的是指向指针的指针。
类比于,传入整型 int a 与传入其指针 int
a的关系,这里是传入指针 ListNode *pHead 和指向指针的指针 ListNode **pHead。

面试题6 从尾到头打印链表

1) 栈
2) 递归 链表过长,可能导致函数调用栈溢出

void PrintListReversingly_Recursively(ListNode* pHead)
{
    if(pHead != nullptr)
    {
        if (pHead->m_pNext != nullptr)
        {
            PrintListReversingly_Recursively(pHead->m_pNext);
        }

        printf("%d\t", pHead->m_nValue);
    }
}

1) 二叉树特例
二叉搜索树

红黑树

面试题7 重建二叉树

1) 已知前序和中序、或者已知后序和中序,都可以递归的形式,通过不断地 划分左右子树然后分别重建这里方法更简洁

面试题8 二叉树的下一个结点

1) 中序遍历:通过画图,分别考虑
2) case 1:存在右子树:下一个是其右子树的最左子结点(E -> K)
3) case 2:不存在右子树:
4) case 2.1:该结点是其父节点的左子结点,则下一个是其父节点(H -> D)
5) case 2.2:该结点是其父节点的右子结点,则下一个是向上溯源,直到溯源结点是其父节点的左子结点,则下一个是溯源结点的父节点(K -> A)
在这里插入图片描述

栈和队列

面试题9 用两个栈实现队列

1) 用两个队列实现栈

template <class T>
class CStack
{
    public: 
        CStack(void);
        ~CStack(void);
        void appendHead(const T& node);
        T deleteHead();

    private:
        queue<T> queue1;
        queue<T> queue2;
};

//入栈和出栈时,至少有一个队列为空
template <class T>
void CStack<T>::appendHead(const T& node)
{
    if(0 == queue1.size())      //向非空的队列push
        queue2.push(node);
    else if(0 == queue2.size())
        queue1.push(node);  
}

template <class T>
T CStack<T>::deleteHead()
{
    if(0 == queue1.size() && 0 == queue2.size())
        throw new exception("stack is empty");

    if(0 == queue1.size())      //从非空队列转移n-1个数据到空队列
    {
        while(1 != queue2.size())
        {
            T& data = queue2.top();
            queue2.pop();
            queue1.push(data);
        }
        T head = queue2.top();
        queue2.pop();
        return head;
    }
    else
    {
        while(1 != queue1.size())
        {
            T& data = queue1.top();
            queue1.pop();
            queue2.push(data);
        }
        T head = queue1.top();
        queue1.pop();
        return head;
    }
}

1.3 算法与数据操作

递归和循环

1) 递归优先
2) 递归,调用栈溢出,子问题重复计算
3) 自上而下的递归,可能出现子问题重复计算的问题,转而采用自下而上的循环

面试题10 斐波那契数列

1) 小青蛙,2^(n-1)
2) 覆盖矩形:f(1)=2,f(0)=0 ?

查找和排序

1) 查找重点:顺序查找、二分查找、哈希表查找、二叉排序树(二叉搜索树)
2) 排序各算法比较

排序 插入排序 冒泡排序 归并排序 快速排序
基本思想 自第二个记录开始,有序前插 相邻元素比较,逆序交换,逐遍把最大值移至最后 相邻两个/多个顺序表合并,初始每个元素都是一个顺序表 两侧依次扫描,将轴值移至正确位置
时间复杂度 n~n^2 n~n^2 nlbn nlbn~n^2
平均时间复杂度 n^2 n^2 nlbn nlbn
空间复杂度 1 1 n lbn~n
应用/优化 适用于元素数量少,且基本有序,改进:分割,分别直插,基本有序后全体直插 双向起泡 相邻-一趟-递归 递归次数-空间复杂度

3) 二分查找

bool BinarySearch(const int r[], const int length, const int searchData)
{
    int start = 0;
    int end = length-1;
    int mid = (start+end)/2;

    while(start<=end)
    {
        if(r[mid]==searchData)
            return true;
        else if(r[mid]>searchData)
            end = mid - 1;
        else
            start = mid + 1;

        mid = (start+end)/2;
    }

    return false;
}

4) 快速排序

//进行一次排序
int Partition(int r[], int length, int start, int end)
{
    if((r==nullptr) || (length<0) || (start<0) || (end>=length))
        throw new std::exception("invalid parameter.");

    int pivot = r[start];   //取第一个值为轴值,挖一个坑
    int i = start;          //左侧扫描指针
    int j = end;            //右侧扫描指针

    while(i<j)
    {
        //右侧扫描:将右侧一个大值移到左侧坑位,空出一个右侧坑位
        while(i<j && pivot<r[j])
            j--;

        if(i<j)
        {
            r[i] = r[j];
            i++;
        }

        //左侧扫描:将左侧一个小值移到右侧坑位,空出一个左侧坑位
        while(i<j && pivot>r[i])
            i++;

        if(i<j)
        {
            r[j] = r[i];
            j--;
        }
    }

    r[i] = pivot;   //把坑位补齐
    return i;
}

void QuickSort(int r[], int length, int start, int end)
{
    if((r==nullptr) || (length<0) || (start<0) || (end>=length))
        throw new std::exception("invalid parameter.");

    if(start < end)
    {
        int index = Partition(r, length, start, end);
        if(start < index) 
            QuickSort(r, length, start, index-1);
        if(index < end)
            QuickSort(r, length, index+1, end);
    }
}

面试题11 旋转数组的最小数字

1) 二分,旋转0个,顺序查找,特例

回溯法

面试题12 矩阵中的路径

#include <cstring>
void* memset( void* dest, int ch, std::size_t count );
/*
Parameters
dest    -   pointer to the object to fill
ch  -   fill byte
count   -   number of bytes to fill
*/

1) 起始位置,for/for
2) 结束条件,‘\0’字符转结尾
3) 约束条件,不能重复走visited
4) 递归,周围四个格子
5) 回溯,pathLength–

面试题13 机器人的运动范围

动态规划与贪婪算法

1) 动态规划:
最优解问题
可分解成子问题,子问题的最优解组合起来得到整个问题的最优解
可能存在重复的子问题
自上而下分析,自下而上求解
2) 贪婪算法:数学证明

面试题14 剪绳子

位运算

1) 移位操作https://blog.csdn.net/weixin_42315600/article/details/80567944
2) 无符号数,左移右添0,右移左添0
有符号数,左移右添0,右移左补符号位
3) 移位效率高于乘除

面试题15 二进制中1的个数

1) 整数减1,再与原数与运算

2 代码质量

2.1 完整性

代码完整性

1) 测试用例
功能测试/边界测试/负面测试
2) 错误处理:返回值、全局变量、C++异常处理

/*使用异常*/
#include <iostream>
using namespace std;
int main()
{
    double m, n;
    cin >> m >> n;
    try 
    {
        cout << "before dividing." << endl;
        if (n == 0)
            throw - 1;  //抛出整型异常
        else if (m == 0)
            throw - 1.0;  //拋出 double 型异常
        else
            cout << m / n << endl;
        cout << "after dividing." << endl;
    }
    catch (double d) 
    {
        cout << "catch (double)" << d << endl;
    }
    catch (...) //捕获任何异常
    {
        cout << "catch (...)" << endl;
    }
    cout << "finished" << endl;
    return 0;
}

/*异常再抛出:
1、不处理
如果一个函数在执行过程中拋出的异常在本函数内就被 catch 块捕获并处理,那么该异常就不会拋给这个函数的调用者(也称为“上一层的函数”);
如果异常在本函数中没有被处理,则它就会被拋给上一层的函数。
2、处理,但继续抛出,如下*/
catch (string s ) 
{
     cout << "CountTax error : " << s << endl;
     throw; //继续抛出捕获的异常,上级调用函数再次去捕获
}

/*C++标准异常类
exception 类的派生类:bad_typeid、bad_cast、bad_alloc、ios_base::failure、out_of_range...
C++ 程序在碰到某些异常时,即使程序中没有写 throw 语句,也会自动拋出上述异常类的对象。
这些异常类还都有名为 what 的成员函数,返回字符串形式的异常描述信息。*/
#include <exception> //exception类,异常类的基类
#include <stdexcept> //logic_error和runtime_error两个公派自exception的类
#include <new>  //bad_alloc公派自exception的类

面试题16 数值的整数次方

1) 考虑:底数为0,指数为负
2) 递归乘方
3) 位运算效率

面试题17 打印从1到最大的n位数

1) 大数问题

Leave a Reply

Your email address will not be published. Required fields are marked *