《剑指Offer(第2版)》读书笔记

目录

  • 第1章 面试的流程
    • 1.3 面试的3个环节
      • 1.3.1 行为面试环节
      • 1.3.2 技术面试环节
        • P12:把一个字符串转换成整数
  • 第2章 面试需要的基础知识
    • 2.2 编程语言
      • 2.2.1 C++
    • 2.3 数据结构
      • 2.3.1 数组
      • 面试题3:数组中重复的数字(题目一)
      • P41:题目二
      • 面试题4:二维数组中的查找
      • 2.3.2 字符串
        • 面试题5:替换空格
      • 2.3.3 链表
        • 面试题6:从尾到头打印链表
      • 2.3.4 树
        • 面试题7:重建二叉树
        • 面试题8:二叉树的下一个节点
      • 2.3.5 栈和队列
        • 栈(stack)
        • 队列(queue)
        • 双向队列(deque)
        • 容器(vector)
        • 面试题9:用两个栈实现队列
    • 2.4 算法和数据操作
      • 2.4.1 递归和循环
        • 面试题10:斐波那契数列
        • 青蛙跳台阶问题
        • 跳台阶扩展问题
      • 2.4.2 查找和排序
        • 排序算法
          • 选择排序——O(n^2)
          • 插入排序——O(n^2)
          • 冒泡排序——O(n^2)
          • 希尔排序——O(n^(1.3—2))
          • 归并排序——O(n*logn)
          • 快速排序——O(n*logn)
          • 各种排序算法的特点
        • 面试题11:旋转数组的最小数字
      • 2.4.3 回溯法
        • 面试题12:矩阵中的路径
        • 面试题13:机器人的运动范围
      • 2.4.4 动态规划与贪婪算法
        • 面试题14:剪绳子
      • 2.4.5 位运算
        • 面试题15:二进制中1的个数
  • 第3章 高质量的代码
    • 3.2 代码的规范性
    • 3.3 代码的完整性
      • 面试题16:数值的整数次方
      • 面试题17:打印从1到最大的n位数
      • 面试题18:删除链表的节点
      • 面试题19:正则表达式匹配
      • 面试题20:表示数值的字符串
      • 面试题21:调整数组顺序使奇数位于偶数前面
      • 面试题21:调整数组顺序使奇数位于偶数前面
    • 3.4 代码的鲁棒性
      • 面试题22:链表中倒数最后k个结点
      • 面试题23:链表中环的入口节点
      • 面试题24:反转链表
      • 面试题25:合并两个排序的链表
      • 面试题26:树的子结构
  • 第4章 解决面试题的思路
    • 4.1 面试官谈面试思路
    • 4.2 画图让抽象问题形象化
      • 面试题27:二叉树的镜像
      • 面试题28:对称的二叉树
      • 面试题29:顺时针打印矩阵
    • 4.3 举例让抽象问题具体化
      • 面试题30:包含min函数的栈
      • 面试题31:栈的压入、弹出序列
      • 面试题32:从上到下打印二叉树
      • 面试题32:把二叉树打印成多行
      • 面试题32:按之字形顺序打印二叉树
      • 面试题33:二叉搜索树的后序遍历序列
      • 面试题34:二叉树中和为某一值的路径
        • 深度优先遍历(DFS)
        • 广度优先遍历(BFS)
    • 4.4 分解让复杂问题简单化
      • 面试题35:复杂链表的复制
      • 面试题36:二叉搜索树与双向链表
      • 面试题37:序列化二叉树
      • 面试题38:字符串的排序
  • 第5章 优化时间和空间效率
    • 5.2 时间效率
      • 面试题39:数组中出现次数超过一半的数字
      • 面试题40:最小的k个数
      • 面试题41:数据流中的中位数
      • 面试题42:连续子数组的最大和
      • 面试题43:1~n整数中1出现的次数
      • 面试题44:数字序列中某一位的数字
      • 面试题45:把数组排成最小的数
      • 面试题46:把数字翻译成字符串
      • 牛客网:JZ49 把字符串转换成整数
      • 面试题47:礼物的最大价值
      • 面试题48:最长不含重复字符的子字符串
    • 5.3时间效率与空间效率的平衡
      • 面试题49:丑数
      • 面试题50:字符串中第一个只出现一次的字符
      • 面试题50:字符流中第一个只出现一次的字符
      • 面试题51:数组中的逆序对
      • 面试题52:两个链表的第一个公共节点
  • 第6章 面试中的各项能力
    • 6.3 知识迁移能力
      • 面试题53:数字在升序数组中出现的次数
      • 面试题53:0~n-1中缺失的数字
      • 面试题53:数组中数值和下标相等的元素
      • 面试题54:二叉搜索树的第k个结点
      • 面试题55:二叉树的深度
      • 面试题55:平衡二叉树
      • 面试题56:数组中只出现一次的两个数字
      • 面试题56:数组中唯一只出现一次的数字
      • 面试题57:和为s的两个数字
      • 面试题57:和为s的连续正数序列
      • 面试题58:翻转字符串:翻转单词顺序
      • 面试题58:翻转字符串:左旋转字符串
      • 面试题59:队列的最大值:滑动窗口的最大值
      • 面试题59:队列的最大值:队列的最大值
    • 6.4 抽象建模能力
      • 面试题60:n个骰子的点数
      • 面试题61:扑克牌中的顺子
      • 面试题62:圆圈中最后剩下的数字
      • 面试题63:股票的最大利润
    • 6.5 发散思维能力
      • 面试题64:求1+2+···+n
      • 面试题65:不用加减乘除做加法
      • 面试题66:构建乘积数组
  • 第7章 两个面试案例
    • 案例一:(面试题67)把字符串转换成整数
    • 案例二:(面试题68)二叉搜索树的最近公共祖先
    • 案例二:(面试题68)二叉树的最近公共祖先

第1章 面试的流程

1.3 面试的3个环节

1.3.1 行为面试环节

P8中间:C++中成员变量的初始化顺序
C++类成员变量初始化顺序:
①基类的静态变量或全局变量
②派生类的静态变量或全局变量
③基类的成员变量
④派生类的成员变量

1.3.2 技术面试环节

P11:传入值参数和传入引用参数有什么区别、什么时候需要为传入的引用参数加上const?

  1. 传值:是把实参的值赋值给形参;那么对形参的修改,不会影响实参的值。
  2. 传引用:真正以地址的方式传递参数,传递以后,形参和实参都是同一个对象,只是它们名字不同而已;对形参的修改将影响实参的值。

原理: 被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
——值传递(pass-by-value)
——引用传递(pass-by-reference)
被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

什么时候要为传入引用参数加上const?
①加上const表示告诉编译器,这个参数是一个常量,首先你在函数内部不能改变它;
②其次,如果在函数内部需要多次引用这个值,CPU不必每次都重新读取,直接使用第一次读取的值;
③如果在需要const引用时,将形参定义为普通引用,则会导致const对象和需要类型转换的对象不能调用该函数,会增加函数的局限性。

P12:把一个字符串转换成整数

class Solution {
public:
    int strToInt(string str)
    {
        //若字符串为空,则退出
        if (str.empty())
            return 0;
        int flag = 1, i = 0, n = str.size(); //flag表示数字的正负
        long long res = 0;
        //当读取到的每个字符索引小于字符的大小 且 字符不为空时,索引++
        while (i < n && str[i] == ' ')
            ++i;
        //若超过字符串的索引值,则退出
        if (i >= n)
            return 0;
        if (str[i] == '-') {
            flag = -1;
            ++i;
        }
        if (str[i] == '+') {
            if (flag == -1)
                return 0; //如果flag已经是-1了,表示是负数,若再遇到+,直接返回前面的数字
            else
                ++i;
        }
        while (str[i] >= '0' && str[i] <= '9')
        {
            res = res * 10 + str[i] - '0';
            ++i;
            if (res > INT_MAX)
                break;
        }
        res *= flag;
        if (res > INT_MAX)
            return INT_MAX;
        if (res < INT_MIN)
            return INT_MIN;
        return res;
    }
};

第2章 面试需要的基础知识

2.2 编程语言

2.2.1 C++

C++中,有哪4个与类型转换相关的关键字?这些关键字各有什么特点,应该在什么场合下使用?
见https://blog.csdn.net/qq_45445740/article/details/110917078
十一 类型转换,2.显示类型转换

P23:关于sizeof的问题
这里书上讲的很详细。

2.3 数据结构

2.3.1 数组

面试题3:数组中重复的数字(题目一)

描述 :在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任一一个重复的数字。
例如,如果输入长度为7的数组[2,3,1,0,2,5,3],那么对应的输出是2或者3。存在不合法的输入的话输出-1

示例1 输入:[2,3,1,0,2,5,3]
返回值:2
说明:2或3都是对的

思路一

采用哈希表,PS:在VS中,使用哈希表要引入头文件
#include
思路:从头到尾按顺序扫描数组的每个数字,每扫描到一个数字的时候,都可以用O(1)的时间来判断哈希表里是否已经包含了该数字。如果哈希表里还没有这个数字,就把它加入哈希表。如果哈希表里已经存在该数字,就找到一个重复的数字。
该算法时间复杂度O(n),空间复杂度O(n)

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param numbers int整型vector 
     * @return int整型
     */
    int duplicate(vector<int>& numbers) {        
        unordered_map<int, int> hash;
        for (int i = 0; i < numbers.size(); i++)
        {
            hash[numbers[i]]++;
            while (hash[numbers[i]] > 1)
                return numbers[i];
        }
        return -1;
    }
};

思路二

采用书上的思路方法P39

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param numbers int整型vector 
     * @return int整型
     */
    int duplicate(vector<int>& numbers) {        
        for(int i = 0;i < numbers.size();i++)
        {
            while(numbers[i] != i)
            {
                if(numbers[i] == numbers[numbers[i]])
                {
                    return numbers[i];
                }
                else{
                    swap(numbers[i],numbers[numbers[i]]);
                }
            }
        }
        return -1;
    }
};

P41:题目二

题目要求在不修改数组的情况下找出重复的数字。
思路一:
采用哈希表,开辟辅助空间,代码和题目一中的哈希表法一样,
时间复杂度O(n),空间复杂度O(n)
思路二:
采用书上的二分查找思路,以时间换空间,
时间复杂度O(nlogn),空间复杂度O(1),源代码见书上。

面试题4:二维数组中的查找

描述 :在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。
给定 target = 3,返回 false。

示例1 输入:
7,[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]
返回值:true
说明:存在7,返回true
示例2 输入:
3,[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]
返回值:false
说明:不存在3,返回false

思路

每次都考虑二维数组右上角的数m,若target小于m,则根据二维数组每一列都按照从上到下递增的顺序排序的特点,则数m这一列都大于m,则该列都不作为考虑的范围;若target大于m,则根据二维数组每一行都按照从左到右递增的顺序排序的特点,则数m这一行都小于m,则该行都不作为考虑的范围。

static const auto io_sync_off = [](){
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    return nullptr;
}();

class Solution {
public:
    bool Find(int target, vector<vector<int> > array) {
        if(array.empty())
            return false;
        int row = 0; //行
        int col = array[0].size() - 1; //列
        while(row < array.size() && col >= 0)
        {
            if(array[row][col] == target)
                return true;
            else if(array[row][col] > target)
                col--;
            else if(array[row][col] < target)
                row++;
        }
        return false;
    }
};

在牛客网上刷题的时候看见速度前几名的代码中都有如下的代码,自己也试了下,发现立马从8ms提升到了4ms,原因参考这个博客:
https://blog.csdn.net/qq_32320399/article/details/81518476

static const auto io_sync_off = [](){
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    return nullptr;
}();

2.3.2 字符串

1.C/C++中每个字符串都以字符’\0’作为结尾,每个字符串中都有一个额外字符的开销,size()和length()返回的长度大小不包括’\0’。
2.关于C/C++中常量字符串
当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址。

面试题5:替换空格

描述 :请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
示例1 输入:
“We Are Happy”
返回值:
“We%20Are%20Happy”

思路一

开辟一个辅助空间,遍历输入字符串的每一个字符,若不是空格字符,则添加进辅助空间,若是空格字符,则在辅助空间添加"%20"字符串。
该算法时间复杂度O(n),空间复杂度O(n)

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return string字符串
     */
    string replaceSpace(string s) {
        string result;
        int len = s.size();
        for(int i = 0;i < len;i++)
        {
            if(s[i] == ' ')
                result += "%20";
            else
                result += s[i];
        }
        return result;
    }
};

思路二

直接在原来的字符串上进行替换,并且保证输入的字符串后面有足够多的空余内存。实现思路:len表示输入字符串的长度,遍历输入字符串的每个字符,每遇到一个空格则len+=2,在原字符串内存的基础上增加内存,遍历原字符串,依次进行赋值,若遇到空格,则赋值"%20"。(书上是字符串从后往前赋值,这里是从前往后)
该算法时间复杂度O(n),空间复杂度O(1)

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return string字符串
     */
    string replaceSpace(string s) {
        int len = s.size();
        int mod_len = len; //定义修改后的字符串长度
        
        for (int i = 0; i < len; i++)
        {
            if (s[i] == ' ')
                mod_len += 2;        
        }
        //char mod_str[mod_len + 1];
        char* mod_str = new char[mod_len + 1]; //因为'\0'也占一个字符
        int num_blank = 0; 
        for (int i = 0; i < len; i++)
        {
            if (s[i] != ' ')
                mod_str[i + 2 * num_blank] = s[i];
            else if (s[i] == ' ')
            {
                mod_str[i + 2 * num_blank] = '%';
                mod_str[i + 2 * num_blank + 1] = '2';
                mod_str[i + 2 * num_blank + 2] = '0';
                num_blank++;
            }
        }
        return mod_str;
        delete[] mod_str;
        mod_str = NULL;
    }
};

2.3.3 链表

内存分配不是在创建链表时一次性完成的,而是每添加一个节点分配一次内存。由于没有闲置的内存,链表的空间效率比数组高。
链表的末尾添加一个节点

struct ListNode {
    int m_nValue;
    ListNode* m_pNext;
};

void AddToTail(ListNode** pHead, int value)
{
    ListNode* pNew = new ListNode(); //声明要添加的新节点
    pNew->m_nValue = value;
    pNew->m_pNext = nullptr;
    
	//当往一个空链表插入一个节点,则新插入的节点就是链表的头指针
    if (*pHead == nullptr)
        *pHead = pNew;
    //当链表不为空,从头节点开始遍历一直到链表的末尾,将pNew插入到链表末尾
    else
    {
        ListNode* pNode = *pHead;
        while (pNode->m_pNext != nullptr)
            pNode = pNode->m_pNext;
        pNode->m_pNext = pNew;
    }
}

单链表的删除
设存储元素ai的节点为q,要实现将节点q删除单链表的操作,其实就是将它的前继节点的指针绕过,指向它的后继节点即可,如下图所示:
《剑指Offer(第2版)》读书笔记_第1张图片
要做的实际上就是一步,p->next = p->next->next,用q来取代p->next,即是:

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

也就是让p的后继的后继节点改成p的后续节点。
在链表中找到第一个含有某值的节点并删除该节点

void RemoveNode(ListNode** pHead, int value)
{
    if (pHead == nullptr || *pHead == nullptr) 
        return;

    ListNode* pToBeDeleted = nullptr;
    if ((*pHead)->m_nValue == value) //如果链表头节点的值等于要找的值,则删除头节点
    {
        pToBeDeleted = *pHead; //先将头节点赋值给pToBeDeleted
        *pHead = (*pHead)->m_pNext; //头节点的下一节点还是头节点,表示把头节点与后面的节点关系先解除,方便后面删除
    }
    else //否则从链表头节点开始遍历要找的值
    {
        ListNode* pNode = *pHead;
        while (pNode->m_pNext != nullptr && pNode->m_pNext->m_nValue != value)
            pNode = pNode->m_pNext;

        if (pNode->m_pNext != nullptr && pNode->m_pNext->m_nValue == value)
        {
            pToBeDeleted = pNode->m_pNext;
            pNode->m_pNext = pNode->m_pNext->m_pNext;
        }
    }
    if (pToBeDeleted != nullptr)
    {
        delete pToBeDeleted;
        pToBeDeleted = nullptr;
    }
}

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

描述 :输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。
示例1 输入:{1,2,3}
返回值:[3,2,1]
示例2 输入:{67,0,24,58}
返回值:[58,24,0,67]

思路一

开辟一个vector辅助空间,从链表头节点开始遍历,依次将每个节点的值存入vector中,翻转vector中的元素。

/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> result;
        ListNode* pNode = head;
        while(pNode)
        {
            result.push_back(pNode->val);
            pNode = pNode->next;
        }
        reverse(result.begin(),result.end());
        return result;
    }
};

思路二

使用栈,“后进先出”的特点

class Solution {
public:
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> res;
        stack<int> nodes;
        while(head)
        {
            nodes.push(head->val);
            head = head->next;
        } 
        while(!nodes.empty())
        {
            res.push_back(nodes.top());
            nodes.pop();
        }           
        return res;
    }
};

2.3.4 树

前序遍历(根左右):先访问根节点,再访问左子节点,最后访问右子节点。
中序遍历(左根右):先访问左子节点,再访问根节点,最后访问右子节点。从小到大的顺序遍历

后序遍历(左右根):先访问左子节点,再访问右子节点,最后访问根节点。
二叉搜索树:左子节点总是小于或等于根节点,而右子节点总是大于或等于根节点。
宽度优先遍历:先访问树的第一层节点,再访问树的第二层节点…一直到访问到最下面一层节点。在同一层节点中,以以从左到右的顺序依次访问。

面试题7:重建二叉树

描述 :输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
示例1 输入:[1,2,3,4,5,6,7],[3,2,4,1,6,5,7]
返回值:{1,2,5,3,4,6,7}

思路
《剑指Offer(第2版)》读书笔记_第2张图片
根据前序遍历和中序遍历的特点,前序遍历的第一个元素就是根节点,根节点后面依次跟着左子树和右子树;所以要先在中序遍历中找到根节点,则在根节点左边就是左子树,根节点右边就是右子树。

DFS思路,依据前序遍历找到根节点,在中序遍历中先找到根节点的位置,在根节点左边是左子树,在根节点右边是右子树,分别构建左、右子树,接下来的事情可以用递归的方法去完成。递归结束的条件是左、右子树的长度大于0。
左子树的长度 = 根节点索引 - 中序遍历头节点
右子树的长度 = 中序遍历尾节点 - 根节点索引
(题目给出的前序遍历、中序遍历的类型是vector,重建二叉树并返回,则要新建一个头节点root)

/**
 * Definition for binary tree
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
        int preLen = pre.size();
        int vinLen = vin.size();
        if(preLen == 0 || vinLen == 0 || preLen != vinLen)
            return nullptr;
        return ConstructBinaryTree(pre, vin, 0, preLen-1, 0, vinLen-1);
    }
    
    TreeNode* ConstructBinaryTree(vector<int> pre,vector<int> vin,int preStart,int preEnd,int InStart,int InEnd)
    {
        int rootValue = pre[preStart]; //前序遍历的第一个元素就是根节点
        TreeNode* root = new TreeNode(rootValue);
        
        //递归终止条件
        if(preStart == preEnd)
            return root;
        
        //在中序遍历中找到根节点的值
        int InrootIndex = 0;
        for(InrootIndex = InStart; InrootIndex <= InEnd; InrootIndex++)
        {
            if(vin[InrootIndex] == rootValue)            
                break;
        }
        
        int leftLen = InrootIndex - InStart; //左子树长度
        int rightLen = InEnd - InrootIndex; //右子树长度
        if(leftLen > 0)
            root->left = ConstructBinaryTree(pre, vin, preStart+1, preStart+leftLen, InStart, InrootIndex-1);
        if(rightLen > 0)
            root->right = ConstructBinaryTree(pre, vin, preStart+leftLen+1, preEnd, InrootIndex+1, InEnd);
        
        return root;
    }
};

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

描述 :给定一个二叉树其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的next指针。下图为一棵有9个节点的二叉树。树中从父节点指向子节点的指针用实线表示,从子节点指向父节点的用虚线表示。
《剑指Offer(第2版)》读书笔记_第3张图片
示例:
输入:{8,6,10,5,7,9,11},8
返回:9
解析:这个组装传入的子树根节点,其实就是整颗树,中序遍历{5,6,7,8,9,10,11},根节点8的下一个节点就是9,应该返回{9,10,11},后台只打印子树的下一个节点,所以只会打印9,如下图,其实都有指向左右孩子的指针,还有指向父节点的指针,下图没有画出来
《剑指Offer(第2版)》读书笔记_第4张图片
输入描述
输入分为2段,第一段是整体的二叉树,第二段是给定二叉树节点的值,后台会将这2个参数组装为一个二叉树局部的子树传入到函数GetNext里面,用户得到的输入只有一个子树根节点
返回值描述
返回传入的子树根节点的下一个节点,后台会打印输出这个节点
示例1
输入:{8,6,10,5,7,9,11},8
返回值:9
示例2
输入:{8,6,10,5,7,9,11},6
返回值:7
示例3
输入:{1,2,#,#,3,#,4},4
返回值:1
示例4
输入:{5},5
返回值:“null”
说明:不存在,后台打印"null"

思路

题目中给出树中的节点不仅包含左右子节点,还有指向父节点的next指针。
根据中序遍历的顺序:左子节点、根节点、右子节点。
①若该节点有右子树,则它的下一个节点一定就是它右子树中的最左节点;
②若该节点没有右子树只有左子树,分两种情况:一是若该节点是其父节点的左子节点,则它的下一个节点就是该点的父节点;二是若该节点是其父节点的右子节点,则需要顺着父节点一直往上遍历,直到找到一个节点是其父节点的左子节点,若这样的点存在即是要找的点,否则返回NULL。

/*
struct TreeLinkNode {
    int val;
    struct TreeLinkNode *left;
    struct TreeLinkNode *right;
    struct TreeLinkNode *next;
    TreeLinkNode(int x) :val(x), left(NULL), right(NULL), next(NULL) {
        
    }
};
*/
class Solution {
public:
    TreeLinkNode* GetNext(TreeLinkNode* pNode) {
        if(pNode == NULL)
            return NULL;
        
        //如果该节点有右子树,则它的下一个节点一定就是右子树中的最左节点
        if(pNode ->right != nullptr)
        {
            pNode = pNode->right;
            while(pNode->left != nullptr)
                pNode = pNode->left;
            return pNode;
        }
        
        //如果没有右子树,只有左子树
        while(pNode->next)
        {
            if(pNode == pNode->next->left)
                return pNode->next;
            pNode = pNode->next;
        }
        return NULL; 
    }
};

2.3.5 栈和队列

栈(stack)

的特点是后进先出,即最后被压入(push)栈的元素会第一个被弹出(pop)。

s.empty();         //如果栈为空则返回true, 否则返回false;
s.size();          //返回栈中元素的个数
s.top();           //返回栈顶元素, 但不删除该元素
s.pop();           //弹出栈顶元素, 但不返回其值
s.push();          //将元素压入栈顶

队列(queue)

队列的特点是先进先出,即第一个进入队列的元素将会第一个出来。

q.empty();         //如果队列为空返回true, 否则返回false     
q.size();          //返回队列中元素的个数
q.front();         //返回队首元素但不删除该元素
q.pop();           //弹出队首元素但不返回其值
q.push();          //将元素压入队列
q.back();          //返回队尾元素的值但不删除该元素

双向队列(deque)

deque双向队列是一种双向开口的连续线性空间,可以高效的在头尾两端插入和删除元素,deque在接口上和vector非常相似。

//数据访问
d.at(idx)          //返回索引idx所指的数据,如果idx越界,抛出out_of_range
d.front()          //返回第一个数据
d.back()           //返回最后一个数据
d.begin()          //返回指向第一个数据的迭代器
d.end()            //返回指向最后一个数据的下一个位置的迭代器
d.rbegin()         //返回逆向队列的第一个数据
d.rend()           //返回指向逆向队列的最后一个数据的下一个位置的迭代器
//加入数据
d.push_back(elem)  //在尾部加入一个数据
d.push_front(elem) //在头部插入一个数据
//删除数据
d.pop_back()       //删除最后一个数据
d.pop_front()      //删除头部数据
d.erase(pos)       //删除pos位置的数据,返回下一个数据的位置
//其他操作
d.empty()          //判断容器是否为空
d.size()           //返回容器中实际数据的个数

容器(vector)

向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence Container)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。

c.clear()            //移除容器中所有数据。
c.empty()            //判断容器是否为空。
c.erase(pos)         //删除pos位置的数据
c.erase(beg,end)     //删除[beg,end)区间的数据
c.front()            //传回第一个数据。
c.insert(pos,elem)   //在pos位置插入一个elem拷贝
c.pop_back()         //删除最后一个数据。
c.push_back(elem)    //在尾部加入一个数据。
c.resize(num)        //重新设置该容器的大小
c.size()             //回容器中实际数据的个数。
c.begin()            //返回指向容器第一个元素的迭代器
c.end()              //返回指向容器最后一个元素的迭代器
c.back()             //返回容器中的最后一个元素

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

描述 :用两个栈来实现一个队列,分别完成在队列尾部插入整数(push)和在队列头部删除整数(pop)的功能。 队列中的元素为int类型。保证操作合法,即保证pop操作时队列内已有元素。
示例 输入:[“PSH1”,“PSH2”,“POP”,“POP”]
返回值:1,2
解析:
“PSH1”:代表将1插入队列尾部
“PSH2”:代表将2插入队列尾部
"POP“:代表删除一个元素,先进先出=>返回1
"POP“:代表删除一个元素,先进先出=>返回2

思路

根据栈后进先出、队列先进先出的特点,队列的push()操作直接由stack1来完成,pop()操作需要先把stack1中的元素翻转,通过将stack1中的元素一个个出栈,再压入stack2中,完成翻转的操作。但要注意,若stack2中本就有元素,则直接出栈顶完成pop()操作。

class Solution
{
public:
    void push(int node) {
        stack1.push(node);
    }

    int pop() {
        if(stack2.empty())
        {
            while(!stack1.empty())
            {
                stack2.push(stack1.top());
                stack1.pop();
            }
        }
        int result = stack2.top();
        stack2.pop();
        return result;
    }

private:
    stack<int> stack1;
    stack<int> stack2;
};

2.4 算法和数据操作

递归的思路
①确定函数的功能
②找出递归结束的条件
③找出函数的等价关系式
关于递归的优化
①若有重复计算,则一定要把计算过的状态保存起来。
②考虑是否可以自底向上

2.4.1 递归和循环

递归是在一个函数的内部调用这个函数自身;而循环则是通过设置计算的初始值及终止条件,在一个范围内重复计算。
递归的代码较简洁,在面试时如无特殊的要求,优先采用递归的方法解决问题。
递归存在的问题:效率较低、重复计算、调用栈溢出。

面试题10:斐波那契数列

描述 :大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。
n≤39
示例 输入:4
返回值:3

思路一

若采用递归算法则时间复杂度大,本题中已经说明了n≤39,所以不需要像书上那样定义long long的类型,解决递归重复运算的方法可以采用自底向上的方法,时间复杂度O(n)。

class Solution {
public:
    int Fibonacci(int n) {
        if(n <= 0)
            return 0;
        if(n <= 2)
            return 1;
        
        int one = 1;
        int two = 1;
        int result = 0;
        for(int i = 3; i <= n; i++)
        {
            result = one + two;
            one = two;
            two = result;
        }
        return result;
    }
};

思路二

解决递归算法则时间复杂度大重复运算的问题,采用将计算过的状态保存起来,时间复杂度O(n)。

class Solution {
public:
    int Fibonacci(int n) {
        vector<int> arr(n+1,0);
        arr[0] = 0;
        arr[1] = 1;
        for(int i=2; i <= n; i++)
            arr[i] = arr[i-1] + arr[i-2];
        return arr[n];
    }
};

青蛙跳台阶问题

描述 :一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

思路一

n=1时,有1种跳法,n=2时,有1+1和2表示的2种跳法,每次跳的时候,青蛙可以跳一个台阶,也可以跳两个台阶,所以每次跳的时候,青蛙有两种跳法。第一种跳法:第一次跳一个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种。第二种跳法:第一次跳两个台阶,那么还剩下n-2个台阶还没,剩下的n-2个台阶的跳法有f(n-2)种。所以,青蛙的全部跳法就是这两种跳法之和,即 f(n) = f(n-1) + f(n-2)。至此,等价关系式就求出来了。
但这么写存在大量重复运算,时间复杂度大。

class Solution {
public:
    int jumpFloor(int number) {
        if(number < 1)
            return 0;
        if(number <= 2)
            return number;
        return jumpFloor(number-1) + jumpFloor(number-2);
    }
};

思路二

通过自己手动列举n=1,2,3,4…,发现跳法的次数分别为:1,2,3,5,8…,很像斐波那契数列,于是采用自底向上的方法。

class Solution {
public:
    int jumpFloor(int number) {
        int one = 1;
        int two = 1;
        int result = 0;
    if(number < 2)
        return number;
        
    for(int i = 2;i <= number;i++)
    {
        result=two + one;
        one = two;
        two = result;
    }
    return result;
    }
};

跳台阶扩展问题

描述 :一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。
示例 输入:3
返回值:4
思路一
自己动笔写一下n=1,2,3,4,5…发现共有1,2,4,8,16…种跳法,满足2^(n-1)。

class Solution {
public:
    int jumpFloorII(int number) {
        int result = 1;
        for(int i=1; i < number; i++)
            result *= 2;
        return result;
    }
};

思路二

看了牛客网前几名的代码,他们找的数字的规律不是2^(n-1),而是f(n) = f(n-1)+f(n-2)+…+f(1)+1,验证后发现果然是这样,4=1+2+1,8=1+2+4+1,16=1+2+4+8+1,代码如下:

class Solution {
public:
    int jumpFloorII(int number) {
        vector<int>dp(number+1,0);
        dp[1]=1;
        dp[2]=2;
        int sum=3;
        for(int i=3;i<=number;i++)
        {
            dp[i]=sum+1;
            sum+=dp[i];
        }
        return dp[number];
    }
};

2.4.2 查找和排序

排序算法

为什么要学习O(n^2)的排序算法?
a.排序的基础
b.编码简单,易于实现,是一些简单情景的首选,在某些情况下,使用的不是高级的设计语言,如汇编语言,在使用性能允许的情况下,O(n^2)排序算法便于实现便成了首选
c.在一些特殊情况下,简单的排序算法更有效
d.简单的排序算法思想衍生出复杂的排序算法,如希尔排序是由插入排序优化而来
e.作为子过程,改进更复杂的排序算法

选择排序——O(n^2)

思路

假设一组数组8,6,2,3,1,5,7,4。首先在这个数组范围里找出第一名的位置,这里假设从小到大排序,然后找到了1,将1和现在的第一名位置8进行交换,变成了1,6,2,3,8,5,7,4,此时第一名的位置就已经确定不用再管了;之后在剩余的部分找到此时最小的元素,找到2之后和第二个位置6进行交换,变成了1,2,6,3,8,5,7,4;这个过程以此类推。

代码:

void selectionSort(int arr[], int len)
{
    for (int i = 0; i < len; i++)
    {
        //寻找[i,n)区间里的最小值
        int minValueIndex = i;
        for (int j = i+1; j < len; j++)
        {
            if (arr[minValueIndex] > arr[j])
                minValueIndex = j;
        }           
        swap(arr[i], arr[minValueIndex]);
    }
}
插入排序——O(n^2)

思路:(类似于玩扑克牌时,整理牌的思想)

假设一组数组8,6,2,3,1,5,7,4。首先对于第一个位置元素8不动,因为一个数字已排好序;之后对于第二个位置元素6,将6与8比较,6比8小,则交换8和6的位置,则数组为6,8,2,3,1,5,7,4,此时前两个元素已排好序;然后对于第三个位置元素2,将2与8比较,2比8小,则交换8和2的位置,则数组为6,2,8,3,1,5,7,4,再将2与6比较,2比6小,则交换6和2的位置,则数组为2,6,8,3,1,5,7,4,此时前三个元素已排好序;这个过程以此类推。

void insertionSort(int arr[], int len)
{
    for (int i = 1; i < len; i++) //对插入排序来说,第一个元素已排好序,直接从第二个元素开始
    {
        for (int j = i; j > 0; j--)
        {
            if (arr[j] < arr[j - 1])
                swap(arr[j], arr[j - 1]);
        }
    }
}
冒泡排序——O(n^2)
void bubbleSort(int arr[], int len)
{
    for (int i = 0; i < len; i++) 
    {
        for (int j = 0; j < len - i - 1; j++)
        {
            if (arr[j] > arr[j + 1])
                swap(arr[j], arr[j + 1]);
        }
    }
}
希尔排序——O(n^(1.3—2))

思路

是基于插入排序的快速排序算法,希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作(且步长要小于数组长度)。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。

void shellSort(int arr[],int n)
{
	int gap;
	gap = n / 2;
	while (gap > 0)
	{
		for (int i = gap; i < n; i++)
		{
			int temp, j;
			temp = arr[i];
			j = i - gap;//arr[i+gap] - arr[i]
			while (j >= 0 && temp < arr[j])
			{
				arr[j + gap] = arr[j];
				j -= gap;
			}
			arr[j + gap] = temp;
		}
		gap /= 2;
	}
}
归并排序——O(n*logn)

思路

归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。
假设一组数组8,4,5,7,1,3,6,2。将其一分为二,则分为2组每组4个数,继续一分为二,则分为4组每组2个数,继续一分为二,则分为8组每组1个数。由于1个数不需要排序,则来到4组每组2个数,对每个组进行排序,则为4,8,5,7,1,3,2,6,然后来到2组每组4个数,对每个组进行排序,则为4,5,7,8,1,2,3,6,最后排序1,2,3,4,5,6,7,8。
再来分析下合并的过程,归并排序需要另开辟一个O(n)的辅助空间,操作如下图:
《剑指Offer(第2版)》读书笔记_第5张图片

//将arr[left...mid]和arr[mid+1...right]两部分进行归并
void merge(int arr[], int left, int mid, int right)
{
	int len = right - left + 1;
	vector<int> temp(len, 0);

	//temp空间是从0开始的,arr这个空间是从left开始的,有left的偏移
	for (int i = left; i <= right; i++)
		temp[i - left] = arr[i];

	int i = left, j = mid + 1;
	for (int k = left; k <= right; k++)
	{
		if (i > mid) //若数组越界,即左边数组大于中间值,表示左边的数值已全部赋值给arr了,剩下的都是右边的数组
		{
			arr[k] = temp[j - left];
			j++;
		}
		else if (j > right) //同理若j索引越界
		{
			arr[k] = temp[i - left];
			i++;
		}
		else if (temp[i - left] < temp[j - left]) //因为有偏移所以要减去left
		{
			arr[k] = temp[i - left];
			i++;
		}
		else {
			arr[k] = temp[j - left];
			j++;
		}
	}
}

//递归使用归并排序,对arr[left...right]
void rec_mergeSort(int arr[], int left, int right)
{
	//设定递归停止条件
	if (left >= right)
		return;
	int mid = (right - left) / 2 + left;

	//对数组的左右两个部分进行归并排序
	rec_mergeSort(arr, left, mid);
	rec_mergeSort(arr, mid + 1, right);
	//数组的左右两边排序后,再对整个进行排序
	merge(arr, left, mid, right);
}

void mergeSort(int arr[], int n)
{
	rec_mergeSort(arr, 0, n - 1); //递归,arr的开始位置0和arr的结束位置n-1
}
快速排序——O(n*logn)

思想
实现快速排序算法的关键在于先在数组中选择一个数字,接下来把数组中的数字分为两部分,比选择的数字小的数字移到数组的左边,比选择的数字大的数字移到数组的右边。依次递归。
程序思路
《剑指Offer(第2版)》读书笔记_第6张图片
《剑指Offer(第2版)》读书笔记_第7张图片
《剑指Offer(第2版)》读书笔记_第8张图片
《剑指Offer(第2版)》读书笔记_第9张图片
在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第10张图片
(随机化快速排序法:原先都是选择数组最左边的元素作为选择快速排序的第一个元素,现在选择随机的元素进行快排。)

//对arr[left...right]部分进行partition操作
//返回p,使得arr[left...p-1] < arr[p];arr[p+1...right] > arr[p]
int partition(int arr[], int left, int right)
{
    //这里选择数组中的第一个数字
    int v = arr[left];
    //arr[left+1...j] < v ; arr[j+1...i-1] > v
    int j = left; //j为
    for (int i = left; i <= right; i++)
    {
        //当arr[i] > v时,因为数字已经在v右边了,不需要任何操作,继续遍历
        if (arr[i] < v)
        {
            swap(arr[j + 1], arr[i]);
            j++;
        }
    }
    swap(arr[j], arr[left]);
    return j;
}

//对arr[left...right]部分进行快速排序
void quickSortPart(int arr[], int left, int right)
{
    //设定递归停止条件
    if (left >= right)
        return;
    //返回一个索引p,p左边小于p,p右边大于p
    int p = partition(arr, left, right); 
    quickSortPart(arr, left, p - 1);
    quickSortPart(arr, p + 1, right);
}

void quickSort(int arr[], int n)
{
    quickSortPart(arr, 0, n - 1);
}

哈希表最主要的优点就是能够在O(1)时间内查找某一元素,是效率最高的查找方式,但其缺点是需要额外的空间来实现哈希表。

各种排序算法的特点

《剑指Offer(第2版)》读书笔记_第11张图片
《剑指Offer(第2版)》读书笔记_第12张图片

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

描述 :把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
示例 输入:[3,4,5,1,2]
返回值:1

思路一

题目中说非递减排序,说明后一个数一定大于等于前一个数,存在相等的情况。按照书上的思路,采用二分法,用两个指针指向数组首和尾,每次都缩小范围,但要考虑特殊情况,如数组旋转了0个元素、首中尾三个指针指向的数相同这两种情况。

class Solution {
public:
    int MinInOrder(vector<int> arr, int index1, int index2)
{
    int result = arr[index1];
    for (int i = index1 + 1; i <= index2; ++i)
    {
        if (result > arr[i])
            result = arr[i];
    }
    return result;
}

int minNumberInRotateArray(vector<int> arr)
{
    int len = arr.size();
    if (len <= 0)
        return -1;
    int left = 0;
    int right = len - 1;
    while (left < right - 1)
    {
        if (arr[left] < arr[right])
        {
            right = left;
            break;
        }

        int middle = left + (right - left) / 2;
        if (arr[left] == arr[right] && arr[middle] == arr[left])
            return MinInOrder(arr, left, right);

        if (arr[middle] >= arr[left])
        {
            left = middle;
        }
        else if (arr[middle] <= arr[right])
        {
            right = middle;
        }
    }
    return arr[right];
}
};

思路二

class Solution {
public:
    int minNumberInRotateArray(vector<int> rotateArray) {
        if(rotateArray.size() == 0)
            return 0;
        int left = 0,right = rotateArray.size()-1;
        while(right > left)
        {
            //如果把原数组的前面的0个元素搬到最后,即排序数组本身,则数组中的第一个数字就是最小的数字
            if(rotateArray[left] < rotateArray[right])
                return rotateArray[left];
            int mid = (right-left)/2 + left;
            if(rotateArray[mid] < rotateArray[right])
                right = mid;
            else if(rotateArray[mid]>rotateArray[right])
                left = mid+1;
            else
                right--;
        }
        return rotateArray[left];
    }
};

2.4.3 回溯法

回溯法非常适合由多个步骤组成的问题,并且每个步骤都有多个选项,在某一步选择了其中一个选型时,就进入下一步,然后又面临新的选型,就这样重复选择,直至到达最终的状态。
通常回溯法算法适合用递归实现代码。

面试题12:矩阵中的路径

描述
《剑指Offer(第2版)》读书笔记_第13张图片
示例1 输入:[[a,b,c,e],[s,f,c,s],[a,d,e,e]],“abcced”
返回值:true
示例2 输入:[[a,b,c,e],[s,f,c,s],[a,d,e,e]],“abcb”
返回值:false
备注(0 <= matrix.length <= 200
0 <= matrix[i].length <= 200)

思路:(DFS

首先考虑若矩阵中只有一个元素的情况,和字符串进行比较;路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格,这里用两个数组来控制左右上下,int dx[4] = { -1,0,1,0 }; int dy[4] = { 0,1,0,-1 };比如dx[0]=-1,dy[0] = 0表示左移一格,这个自己定义也可以写成int dx[4] = {-1,0,1,0}; int dy[4] = {0,-1,0,1};若找到了符合字符串的第一个字符,就继续往下找,直到找到字符串的结尾’\0’,说明包含该路径,则返回true,若中途找到一个不符合,就返回到上一步,但要注意为防止回头走,走过的路径置为星号,若此条路不通,遍历完之后返回去,还要把该路径上的元素进行还原。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param matrix char字符型vector> 
     * @param word string字符串 
     * @return bool布尔型
     */
    //参数visited表示字符串中的索引
    bool dfs(vector<vector<char>>& matrix, int rows, int cols, string word, int visited, int x, int y)
    {
        //如果访问到word中的'\0',说明到了字符串结尾,则整个字符串都存在
        if(word[visited] == '\0')
            return true;

        //用来控制上下左右四个方向
        int dx[4] = { -1,0,1,0 };
        int dy[4] = { 0,1,0,-1 };
        for (int i = 0; i < 4; i++) //实现上下左右遍历
        {
            int a = x + dx[i], b = y + dy[i];
            if (a >= 0 && a < rows && b >= 0 && b < cols && matrix[a][b] == word[visited])
            {
                char temp = matrix[a][b];
                matrix[a][b] = '*'; //防止往回走
                if (dfs(matrix, rows, cols, word, visited + 1, a, b)) //如果相等则字符往后走
                    return true;
                matrix[a][b] = temp; //遍历完后还得还原原来的字符
            }
        }
        return false;
    }

    bool hasPath(vector<vector<char> >& matrix, string word)
    {
        int rows = matrix.size();
        int cols = matrix[0].size();

        //对于特殊情况如矩阵中只有一个元素时
        if (rows == 1 && cols == 1)
        {
            if (matrix[0][0] == word[0])
                return true;
            else
                return false;
        }

        for (int i = 0; i < rows; i++)
            for (int j = 0; j < cols; j++)
                if (dfs(matrix, rows, cols, word, 0, i, j))
                    return true;
        return false;
    }
};

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

《剑指Offer(第2版)》读书笔记_第14张图片
int movingCount(int threshold, int rows, int cols)

示例1
输入:1,2,3
返回值:3
示例2
输入:0,1,3
返回值:1
示例3
输入:10,1,100
返回值:29
说明:
[0,0],[0,1],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[0,8],[0,9],[0,10],[0,11],[0,12],[0,13],[0,14],[0,15],[0,16],[0,17],[0,18],[0,19],[0,20],[0,21],[0,22],[0,23],[0,24],[0,25],[0,26],[0,27],[0,28] 这29种,后面的[0,29],[0,30]以及[0,31]等等是无法到达的
示例4
输入:5,10,10
返回值:21

思路:(回溯BFS

这题不是遍历所有方格看是否行列数位之和满足条件,而是随着机器人的移动,有些位置虽然满足不大于threshold,但却是机器人到达不了的位置,中间有间隔的。
这里的选用stack只是为了让这个循环判断下去的载体,用来存储每次的坐标点,换成队列也行。

class Solution {
public:
    int movingCount(int threshold, int rows, int cols) {
        if(threshold < 0 || rows <=0 || cols <= 0)
            return 0;
        
        int result = 0;
        stack<pair<int,int>> p; //p用来记录坐标点,pair是将2个数据组合成一组数据
        vector<vector<bool>> grid(rows,vector<bool>(cols,false));//定义m行n列的方格,初始化都为false,表示未走过
        
        //BFS
        int dx[4] = {-1,0,1,0};
        int dy[4] = {0,1,0,-1};
        p.push({0,0}); //先把(0,0)点push进入,让循环开始
        while(p.size())
        {
            //把栈中的坐标拿出来进行判断
            pair<int,int> temp = p.top();
            p.pop();
            
            //如果这个格子已经走过即为true 或者 行列坐标的数位之和大于threshold,则continue继续当前循环
            if(grid[temp.first][temp.second] || getDigitSum(temp.first)+getDigitSum(temp.second) > threshold)
                continue;
            
            result ++;
            grid[temp.first][temp.second] = true; //符合要求的格子置为true,代表走过,防止回走重复计算
            
            for(int i=0; i<4; i++)
            {
                int a = temp.first+dx[i];
                int b = temp.second+dy[i];
                if(a >=0 && a < rows && b >=0 && b < cols)
                    p.push({a,b});
            }
        }
        return result;
    }
    
    int getDigitSum(int number)
    {
        int sum = 0;
        while(number > 0)
        {
            sum += number%10;
            number /= 10;
        }
        return sum;
    }
};

2.4.4 动态规划与贪婪算法

如果面试题是求一个问题的最优解(通常是求最大值或者最小值),而且该问题能够分解成若干个子问题,并且子问题之间还有重叠的更小的子问题,就可以考虑用动态规划来解决这个问题。
PS:在分解为子问题分别求解时,为了避免重复求解子问题,需要将其存储起来,从上往下分析问题,从下往上求解问题,这个和递归的优化类似。

面试题14:剪绳子

《剑指Offer(第2版)》读书笔记_第15张图片

示例1
输入:8
返回值:18

思路一动态规划

书上的思路,采用动态规划。为避免重复计算,将求解的子问题的结果保存起来,注意题目中要求至少切成2段,这个保存的子问题结果初始化不一样。

class Solution {
public:
    int cutRope(int number) {
        //因为题目中要求m>1,所以至少切成2段,2:1*1,3:1*2
        if(number <= 3)
            return number-1;
        
        vector<int> len(60);
        //下面是初始化的值,不是子段数
        len[0] = 0;
        len[1] = 1;
        len[2] = 2;
        len[3] = 3;
        int max = 0; //记录最大的乘积
        for (int i = 4; i <= number; i++)
        {
            for (int j = 1; j <= i / 2; j++)
            {
                int temp = len[j] * len[i - j];
                if (max < temp)
                    max = temp;
                len[i] = max;
            }
        }
        return len[number];
    }   
};

思路二

书上的思路,采用数学的方法。

class Solution {
public:
    int cutRope(int number) {
             //即对绳子进行3的取余
        if (number <= 3)
            return number - 1;
        int max = 1;
        if (number % 3 == 1) //如果绳长为4
        {
            max *= 4;
            number -= 4;
        }
        else if (number % 3 == 2) //5
        {
            max *= 2;
            number -= 2;
        }
        while (number)
        {
            max *= 3;
            number -= 3;
        }
        return max;
        }   
};

2.4.5 位运算

①异或(^):相同为0,不同为1。
②左移:在左移n位的时候,最左边的n位将被丢弃,同时在最右边补上n个0。
③右移:在右移n位的时候,最右边的n位将被丢弃,但右移时处理最左边位的情形要稍微复杂一点。如果数字是一个无符号数值,则用0填补最左边的n位;如果数字是一个有符号数值,则用数字的符合位填补最左边的n位。也就是说,如果数字原先是一个正数,则右移之后在最左边补n个0;如果数字原先是负数,则右移之后在最左边补n个1。如:

00001010>>2 = 00000010
10001010>>3 = 11110001

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

《剑指Offer(第2版)》读书笔记_第16张图片
思路一

考虑负数,就比如-9 补码是11110111。
9的源码为00001001,如果是负数的话,补码为最高位置1,
其余取反也就是11110110,
然后在最低位加1即可即11110111。
计算机中的负数是以其补码形式存在的 补码=原码取反+1
这里是32位,则-9的补码是11111111 11111111 11111111 11110111,
则输入:9,返回31。
思路见书上:首先把n和1做与运算,判断n的最低位是不是为1。接着把1左移一位得到2,再和n做与运算,就能判断n的次低位是不是1…这样反复左移,每次都能判断n的其中一位是不是1。

class Solution {
public:
     int  NumberOf1(int n) {
         int count = 0;
         unsigned int flag = 1;
         while(flag)
         {
             if(n & flag)
                 count++;
             flag = flag<<1;
         }
         return count;
    }
};

思路二

书上的方法,不用考虑输入是正数还是负数,总结就是:把一个整数减去1之后再和原来的整数做位与运算,得到的结果相当于把整数的二进制表示中最右边的1变成0

class Solution {
public:
     int  NumberOf1(int n) {
         int count = 0;
         while(n)
         {
             count++;
             n = n & (n-1);
         }
         return count;
    }
};

第3章 高质量的代码

3.2 代码的规范性

通用命名规则:函数命名, 变量命名, 文件命名要有描述性; 少用缩写。
说明:尽可能使用描述性的命名。
(PS:一些特定的广为人知的缩写是允许的, 例如用 i 表示迭代变量和用 T 表示模板参数。)
变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如:

a_local_variable, a_struct_data_member, a_class_data_member_

3.3 代码的完整性

完整的代码=功能测试+边界测试+负面测试(非法输入)

面试题16:数值的整数次方

《剑指Offer(第2版)》读书笔记_第17张图片

示例1
输入:2.00000,3
返回值:8.00000
示例2
输入:2.10000,3
返回值:9.26100
示例3
输入:2.00000,-2
返回值:0.25000
说明:2的-2次方等于1/4=0.25

思路

考虑负数

class Solution {
public:
    double Power(double base, int exponent) {
        if (exponent == 0)
            return 1.0;
        double result = 1;
        for (int i = 0; i < abs(exponent); i++)
            result *= base;
        if (exponent < 0)
            result = 1 / result;

        return result;
    }
};

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

(要考虑大数问题,leetcode中这道题返回类型是int类型,未考虑到大数问题。)
《剑指Offer(第2版)》读书笔记_第18张图片
思路:

书上的思路,在数字前面补0,即可得到n位所有十进制数其实就是n个从0到9的全排列,也就是说把数字的每一位都从0到9排列一遍,就得到了所有的十进制数。只是在打印的时候,排在前面的0不打印出来。
全排列用递归实现,递归的终止条件是已经设置了数字到的最后一位,数字的每一位都是’0’~'9’之间的一个数,然后设置下一位。

class Solution {
public:
    void Print1ToMaxOfNDigits(int n)
    {
        if (n <= 0)
            return;

        char* number = new char[n + 1];
        number[n] = '\0'; //数字最大是n位,并设n位数初始化为'0',因为字符串中最后一位是结束符号'\0'

        for (int i = 0; i < 10; ++i)
        {
            number[0] = i + '0'; //控制首位'0'~'9'
            Print1ToMaxOfNDigitsRecursively(number, n, 0);
        }

        delete[] number;
    }

    void Print1ToMaxOfNDigitsRecursively(char* number, int length, int index)
    {
        //递归终止条件:如果索引到了字符串的最后一位,就打印这个数,即已经设置了数字的最后一位
        if (index == length - 1)
        {
            PrintNumber(number);
            return;
        }

        for (int i = 0; i < 10; ++i)
        {
            number[index + 1] = i + '0'; //控制剩下的每位'0'~'9'
            Print1ToMaxOfNDigitsRecursively(number, length, index + 1);
        }
    }

    // 字符串number表示一个数字,数字有若干个0开头
    // 打印出这个数字,并忽略开头的0
    void PrintNumber(char* number)
    {
        bool isBeginning0 = true; //设置第一个为非'0'的字符,标志位
        int nLength = strlen(number);

        for (int i = 0; i < nLength; ++i)
        {
            //找到第一个非'0'的字符后就把标志位设为false,这样就不会进入到这个for循环,因为只要忽略左边的'0',之后再出现'0'是要打印的
            if (isBeginning0 && number[i] != '0')
                isBeginning0 = false;

            if (!isBeginning0)
            {
                cout << number[i];
            }
        }
        cout << " ";
    }
};

面试题18:删除链表的节点

在这里插入图片描述

示例1
输入:{1,2,3,3,4,4,5}
返回值:{1,2,5}

思路

首先初始化一个虚拟的头节点,用来防止如果头节点也是重复节点被删除的情况。
通过双指针的方法进行求解,用指针p和q。
可以按照程序的逻辑自己举例。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead) {
        ListNode* dummy = new ListNode(-1); //初始化一个节点值为-1的空节点,建立虚拟头节点,避免头节点被删除的情况
        dummy->next = pHead;
        
        ListNode* p = dummy;
        while(p->next)
        {
            ListNode* q = p->next;
            while(q && p->next->val == q->val)
                q = q->next;
            
            //p->next->next == q表示指针p和q之间的距离为1,说明没有重复节点,则指针p往后移一位
            if(p->next->next == q)
                p = p->next;
            //如果距离大于1,说明有重复的数字,则p的下一个节点直接指向q,跳过了中间相同的节点
            else
                p->next = q;
        }
        return dummy->next;
    }
};

面试题19:正则表达式匹配

《剑指Offer(第2版)》读书笔记_第19张图片

示例1
输入:“aaa”,“a*a”
返回值:true

思路
《剑指Offer(第2版)》读书笔记_第20张图片

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param str string字符串 
     * @param pattern string字符串 
     * @return bool布尔型
     */
    bool matchCore(string str, string pattern, int strindex, int patindex) {
    //如果字符串和模式串都匹配到了最后,说明匹配成功
        if (strindex == str.size() && patindex == pattern.size())
            return true;
        //如果模式串已经匹配到了最后,但字符串还没匹配完,说明匹配失败
        if (strindex != str.size() && patindex == pattern.size())
            return false;

        if (pattern[patindex + 1] == '*')
        {
            if (pattern[patindex] == str[strindex] || (pattern[patindex] == '.' && str[strindex] != '\0'))
                return matchCore(str, pattern, strindex + 1, patindex + 2) || matchCore(str, pattern, strindex + 1, patindex) || matchCore(str, pattern, strindex, patindex + 2);
            else
                return matchCore(str, pattern, strindex, patindex + 2);
        }

        if (str[strindex] == pattern[patindex] || pattern[patindex] == '.' && str[strindex] != '\0')
            return matchCore(str, pattern, strindex + 1, patindex + 1);

        return false;
    }

    bool match(string str, string pattern) {
        if (str == "\0" && pattern == "\0")
            return true;
        if (str != "\0" && pattern == "\0")
            return false;
        return matchCore(str, pattern, 0, 0);
    }
};

面试题20:表示数值的字符串

《剑指Offer(第2版)》读书笔记_第21张图片
思路

一路if else下去,考虑多种不是的情况。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param str string字符串 
     * @return bool布尔型
     */
    bool isNumeric(string str) 
    {
        if(str == "\0")
            return false;
        return isNumericCore(str,0);
    }
    
    bool isNumericCore(string str,int i)
    {
        //若-+号连续出现两次(-- +- -+ ++),返回false
        if(str[i] == '+' || str[i] == '-')
        {
            i++;
            if(str[i] == '+' || str[i] == '-')
                return false;
        }
        
        //若出现+或者-号后没有数字了,返回false
        if(str[i] == '\0') return false;
        
        int dot = 0, e = 0, num = 0; //定义小数点个数、e的个数、整数的个数
        while(str[i] != '\0') //遍历整个字符串
        {
            if(str[i] >= '0' && str[i] <= '9') 
            {
                i++;
                num++; //整数个数自加1
            }
            else if(str[i] == '.')
            {
                //排除掉连续两个小数点、e后面出现小数点、小数点后没有数字的情况
                if(dot > 0 || e > 0 || str[i+1] == '\0')
                    return false;
                i++;
                dot++;
            }
            else if(str[i] == 'e' || str[i] == 'E')
            {
                //排除掉e前面没有数字和出现两个e的情况
                if(num == 0 || e > 0)
                    return false;
                i++;
                e++;
                //下面判断e之间是不是出现了连续的+-号
                if(str[i] == '+' || str[i] == '-')
                    {
                        i++;
                        if(str[i] == '+' || str[i] == '-')
                            return false;
                    }
                //如果e后面没有数字了,也返回false
                if(str[i] == '\0')
                    return false;
            }
            else
                return false;
        }
        return true;
    }
};

面试题21:调整数组顺序使奇数位于偶数前面

《剑指Offer(第2版)》读书笔记_第22张图片
思路

这题和书上不同,这题还要求奇数和奇数,偶数和偶数之间的相对位置不变。
开辟两个队列的辅助空间,用来存储数组中的奇数和偶数。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param array int整型vector 
     * @return int整型vector
     */
    vector<int> reOrderArray(vector<int>& array) {
        int len = array.size();
        queue<int> oddQueue,evenQueue;
        for(int i = 0; i < len; i++)
        {
            if(array[i] % 2 == 1)
                oddQueue.push(array[i]);
            else
                evenQueue.push(array[i]);
        }
        array.clear();
        while(!oddQueue.empty())
        {
            array.push_back(oddQueue.front());
            oddQueue.pop();
        }
        while(!evenQueue.empty())
        {
            array.push_back(evenQueue.front());
            evenQueue.pop();
        }
        return array;
    }
};

面试题21:调整数组顺序使奇数位于偶数前面

思路

书上这题的做法:采用双指针,第一个指针初始化时指向数组的第一个数字,它只向后移动;第二个指针初始化时指向数组的最后一个数字,它只向前移动。在两个指针相遇之前,第一个指针总是位于第二个指针的前面。如果第一个指针指向的数字是偶数,并且第二个指针指向的数字是奇数,则交换这两个数字。
考虑第一个指针偶数,第二个指针奇数;第一个指针偶数,第二个指针偶数;第一个指针奇数,第二个指针奇数;第一个指针奇数,第二个指针偶数。)

vector<int> reOrderArray(vector<int>& arr) 
{
    int len = arr.size();
    int start = 0;
    int end = len - 1;
    while (start < end)
    {
        if (arr[start] % 2 == 0 && arr[end] % 2 == 1)
        {
            swap(arr[start], arr[end]);
            start++;
            end--;
        }
        else if (arr[start] % 2 == 1)
            start++;
        else if (arr[end] % 2 == 0)
            end--;
    }
    return arr;
}

3.4 代码的鲁棒性

面试题22:链表中倒数最后k个结点

《剑指Offer(第2版)》读书笔记_第23张图片
思路

题目要求输出一个链表,即输出链表中倒数最后的第k个节点的指针。先遍历整个链表获取链表的长度num,然后从头结点开始遍历到第num-k个节点位置,就是返回值。

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pHead ListNode类 
     * @param k int整型 
     * @return ListNode类
     */
    ListNode* FindKthToTail(ListNode* pHead, int k) {
        if(pHead == nullptr)
            return nullptr;
        int num = 0; //计算整个链表的长度
        for(ListNode* p=pHead; p != nullptr; p = p->next)
            num++;
        
        if(k > num)
            return nullptr;
        
        ListNode* p = pHead;
        for(int i=0; i < num-k; i++)
            p = p->next;
        
        return p; //从p节点至尾节点,都是返回的链表
    }
};

面试题23:链表中环的入口节点

《剑指Offer(第2版)》读书笔记_第24张图片
《剑指Offer(第2版)》读书笔记_第25张图片
思路一(快慢指针)

采用书上的思路,用两个指针解决。
首先对该链表进行判断,若为空链表,则直接返回NULL;
然后①第一步确定这个链表是否有环。可以像书上那样说的用两个指针来解决,一个快指针,一个慢指针,同时从链表的头节点出发,快指针一次走两步,慢指针一次走一步,快指针走到了链表的末尾(即next == NULL),则说明链表不闭环,返回NULL。(其实也可以用一个指针不断遍历下去,next==NULL就说明不闭环,但为了获得相遇节点,定义了快慢指针。)
其次②如何获取闭环的长度。之前定义的快慢指针肯定在环中相遇,记录下该节点meetNode,计数,从meetNode节点继续向下遍历,再次遇到meetNode节点时,走过的步数就是该节点的长度。
最后③定义两个节点pNode1和pNode2,pNode2先走闭环长度的步数,接下来两个节点都以相同的速度在链表上移动,它们相遇的节点就是环的入口节点。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        if(pHead == NULL)
            return NULL;
        ListNode* slow = pHead;
        ListNode* fast = pHead;
        ListNode* meetNode = nullptr;
        //1.确定这个链表是否有环
        while(slow && fast)
        {
            slow = slow->next;
            fast = fast->next;
            if(fast)
                fast = fast->next;
            else
                return NULL;
            
            if(slow == fast)
            {
                meetNode = slow;
                break;
            }
        }
        //2.获取闭环的长度
        int num = 1;
        ListNode* pNode = meetNode;
        while(pNode->next != meetNode)
        {
            pNode = pNode->next;
            num++;
        }
        //3.得到环的入口节点
        ListNode* pNode1 = pHead;
        ListNode* pNode2 = pHead;
        for(int i=0; i<num; i++)
            pNode2 = pNode2->next;
        while(pNode1 != pNode2)
        {
            pNode1 = pNode1->next;
            pNode2 = pNode2->next;
        }
        return pNode1;
    }
};

简化下上面的代码,即快慢指针第一次相遇的节点的位置,就是从头节点往后走环的长度大小的位置

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        if(pHead == NULL)
            return NULL;
        ListNode* slow = pHead;
        ListNode* fast = pHead;
        
        while(slow && fast)
        {
            slow = slow->next;
            fast = fast->next;
            if(fast)
                fast = fast->next;
            else
                return NULL;
            
            if(slow == fast)
            {
                slow = pHead;
                while(fast != slow)
                {
                    fast = fast->next;
                    slow = slow->next;
                }
                return slow;
            }
        }
        return NULL;
    }
};

思路二(哈希表)

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        unordered_map<ListNode*, int> hash;
        int id = 1;
        for(auto i=pHead; i ; i=i->next,id++)
        {
            if(hash[i] != 0)
                return i;
            else
                hash[i] = id;
        }
        return NULL;
    }
};

面试题24:反转链表

(面试高频题)
《剑指Offer(第2版)》读书笔记_第26张图片

思路

一个链表中,只有next属性,下一个节点, 现在要进行反转,将当前节点指向前一个节点pre,在反转当前节点前,需要将当前节点的下一个节点next进行保存,因为反转之后就没有next属性。
反转节点后,pre、pHead、next,需要将pHead赋值给pre,next赋值给pHead,是为了对下一个节点进行反转操作。
最后返回pre。

class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        if(pHead == nullptr)
            return nullptr;
        
        ListNode* pre = nullptr;
        ListNode* next = nullptr;
        while(pHead)
        {
            next = pHead->next; //提前保存下pHead节点的下一个节点
            pHead->next = pre; //反转节点
            
            //将节点前移,方便处理下一个节点
            pre = pHead;
            pHead = next;
        }
        return pre;
    }
};

面试题25:合并两个排序的链表

《剑指Offer(第2版)》读书笔记_第27张图片
思路

书上思路,首先对边界值进行处理,第一个链表为空、第二个链表为空、两个链表都为空时的返回结果。
采用归并的思路,从两个链表的头节点开始一个个比较,递归下去。
当得到两个链表中值较小的头节点并把它链接到已经合并的链表之后,两个链表剩余的节点依然是排序的,因此合并的步骤和之前的步骤是一样的,可以定义递归函数完成这一合并过程。

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        if(pHead1 == NULL && pHead2 == NULL)
            return NULL;
        if(pHead1 == NULL)
            return pHead2;
        else if(pHead2 == NULL)
            return pHead1;
        
        ListNode* pNode = nullptr;
        if(pHead1->val < pHead2->val)
        {
            pNode = pHead1;
            pNode->next = Merge(pHead1->next, pHead2);
        }
        else{
            pNode = pHead2;
            pNode->next = Merge(pHead2->next, pHead1);
        }
        return pNode;
    }
};

面试题26:树的子结构

《剑指Offer(第2版)》读书笔记_第28张图片
思路

题目已经说了是A包含B的关系,先判断边界条件,若A和B有一个为空,则返回false。
第一步,在树A中找到和树B的根节点的值一样的节点R;
第二步,判树A中以R为根节点的子树是不是包含和树B一样的结构。
程序思路:首先对边界值进行判断;然后对树A和树B的根节点的值进行判断是否相等,若相等,则继续遍历A和B根节点的左右子树的结构是否都相等,若都一样则返回true。若A和B的根节点不相等,则遍历树A在其中找到与树B根节点相等的节点R,通过递归判断A和B各自的左右节点的值是不是相同。递归的终止条件是达到了树A或者树B的叶节点。

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
        if(pRoot1 == NULL || pRoot2 == NULL)
            return false;
        if(DoesTree1HasTree2(pRoot1,pRoot2)) 
            return true;
        
        return HasSubtree(pRoot1->left,pRoot2) || HasSubtree(pRoot1->right, pRoot2);
    }
    
    bool DoesTree1HasTree2(TreeNode* pRoot1,TreeNode* pRoot2) 
    {
        if(!pRoot2)  //若树B已经遍历到了叶节点结束,说明A包含B
            return true;
        if(!pRoot1)  //若树A已经遍布到了叶节点结束都没有与B相等的,则返回false
            return false;
        if(pRoot1->val != pRoot2->val)  //若两节点值不同
            return false;
        return DoesTree1HasTree2(pRoot1->left,pRoot2->left) && DoesTree1HasTree2(pRoot1->right,pRoot2->right);
    }
};

第4章 解决面试题的思路

4.1 面试官谈面试思路

应聘者在写代码之前,一定要和面试官讲自己的思路想法,还有一些边界问题等都可以和面试官沟通。

4.2 画图让抽象问题形象化

在面试的时候,如若遇到复杂的问题光用语言未必能够说清楚,这时候可以通过画图、举例的方式。

面试题27:二叉树的镜像

《剑指Offer(第2版)》读书笔记_第29张图片
思路

所有节点的左右孩子节点都和源二叉树相比交换了位置。
程序思路:把源二叉树所有节点都遍历一遍,交换左右孩子节点。

/**
 * struct TreeNode {
 *	int val;
 *	struct TreeNode *left;
 *	struct TreeNode *right;
 *	TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pRoot TreeNode类 
     * @return TreeNode类
     */
    TreeNode* Mirror(TreeNode* pRoot) {
        if(pRoot == NULL)
            return NULL;
        Mirror(pRoot->left);
        Mirror(pRoot->right);
        swap(pRoot->left,pRoot->right);
        return pRoot;
    }
};

面试题28:对称的二叉树

在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第30张图片
思路:

借助上一题二叉树镜像的思路,当所有节点的左右孩子节点的值都相等,即镜像,则说明是对称的二叉树。
程序思路:首先进行边界值判断,当输入是一个空的二叉树,则返回true;然后遍历二叉树的所有节点进行左右孩子节点的值是否相等的判断,这里调用递归,注意递归的终止条件,只有当pNodeleft和pNoderight节点都为空时(说明都遍历到了左右节点的最后),返回true,若其中一个为空都返回false(说明左右不对称),这里统一写成了return !pNodeleft && !pNoderight。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    bool isSymmetrical(TreeNode* pRoot) {
        if(pRoot == nullptr)
            return true;
        return isEqual(pRoot->left, pRoot->right);
    }
    
    bool isEqual(TreeNode* pNodeleft,TreeNode* pNoderight)
    {
        if(!pNodeleft || !pNoderight)
            return !pNodeleft && !pNoderight;
        if(pNodeleft->val != pNoderight->val)
            return false;        
        return isEqual(pNodeleft->left, pNoderight->right);
    }
};

面试题29:顺时针打印矩阵

《剑指Offer(第2版)》读书笔记_第31张图片
思路:(DFS)

程序思路:
首先函数是vector类型,返回一个vector类型的数组result,
若输入是空,则返回result为空。
关于标记已经打印的元素,不能将走过的路径置为0,万一原矩阵中就有元素等于0,所有这里用一个bool数组来表示,未走过置为false,走过置为true。
定义移动的方向,因为是顺时针,则为右下左上,dx[4] = {0,1,0,-1}, dy[4] = {1,0,-1,0};x表示行数即纵坐标,y表示列数即横坐标,dx[0]=0,dy[0]=1,列数+1,表示右移一位。

class Solution {
public:
    vector<int> printMatrix(vector<vector<int> > matrix) {
        vector<int> result;
        if(matrix.empty())
            return result;
        
        int rows = matrix.size(); //矩阵行数
        int cols = matrix[0].size(); //矩阵列数
        vector<vector<bool>> state(rows, vector<bool>(cols,false)); //定义矩阵每个位置的初始状态,判断有没有走过
        
        //因为是顺时针,定义方向:右下左上
        int dx[4] = {0,1,0,-1}, dy[4] = {1,0,-1,0};
        int x=0, y=0, d=0; //定义开始坐标x,y,方向d
        for(int i=0; i<rows*cols; i++)
        {
            result.push_back(matrix[x][y]);
            state[x][y] = true;
            
            int a = x+dx[d], b = y+dy[d];
            if(a<0 || a>=rows || b<0 || b>=cols || state[a][b] == true)
            {
                d = (d+1)%4;
                a = x+dx[d], b = y+dy[d];
            }
            x = a;
            y = b;
        }
        
        return result;
    }
};

4.3 举例让抽象问题具体化

面试题30:包含min函数的栈

《剑指Offer(第2版)》读书笔记_第32张图片
思路:

新建两个栈s1和s2,s1作为正常的数据栈执行push()、pop()、top()操作,s2用来存储当前数据栈s1的最小值。
(PS:这题和书上有一处不一样,如果新压入栈的元素更小,则s2压入value,如果新压入栈的数字比之前的最小值大,则压入s2的栈顶。但这题不这么写,也能通过)

class Solution {
private:
    stack<int> s1;
    stack<int> s2;
public:
    void push(int value) {
        s1.push(value);
        if(s2.empty() || value <= s2.top()) //这里要注意是<=,不能忘了等于的情况,否则在pop()时,会报错
            s2.push(value);
    }
    void pop() {
        if(s1.top() == s2.top())
            s2.pop();
        s1.pop();
    }
    int top() {
        return s1.top();
    }
    int min() {
        return s2.top();
    }
};

面试题31:栈的压入、弹出序列

《剑指Offer(第2版)》读书笔记_第33张图片
《剑指Offer(第2版)》读书笔记_第34张图片
思路

输入的是两个整数序列vector类型。
首先对边界值进行判断,若输入的两个数组长度不一样,则直接返回false;
新建一个辅助栈s,用两个指针p1和p2分别指向pushV和popV;
辅助栈s用来存入pushV中的数,当下一个弹出的数字刚好是栈顶数字,即辅助栈s的栈顶和popV[p2]相同,则s.pop()弹出栈顶并且p2指针往后移一格开始比较下一个数字;如果下一个弹出的数字不在栈顶,则就把压栈数组pushV中还没有入栈的数字压入辅助栈s,每压入一个数字,指针p1往后移一格,直到把下一个需要弹出的数字压入栈顶为止,压入后再弹出来。
(因为一开始辅助栈s为空,先把pushV的第一个元素先压入栈;while循环的终止条件是指针不能走过超过数组的长度,最后若辅助栈s为空,说明全都按照popV的顺序弹出,返回true,否则说明popV不可能是一个弹出序列,返回false。)

class Solution {
public:
    bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        if(pushV.size() != popV.size())
            return false;
        
        stack<int> s;
        int p1 = 0;
        int p2 = 0;
        s.push(pushV[p1]);
        
        while(p1 < pushV.size() && p2 < popV.size())
        {
            if(!s.empty() && s.top() == popV[p2])
            {
                s.pop();
                p2++;
            }
            else
            {
                p1++;
                s.push(pushV[p1]);                
            }
        }
        
        return s.empty();
    }
};

下面这样写好像更简洁:

class Solution {
public:
    bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        if(pushV.size() != popV.size())
            return false;
        
        stack<int> s;
        int i = 0;
        
        for(auto x:pushV)
        {
            s.push(x);
            while(s.size() && s.top() == popV[i])
            {
                s.pop();
                i++;
            }
        }
        
        return s.empty();
    }
};

面试题32:从上到下打印二叉树

《剑指Offer(第2版)》读书笔记_第35张图片
思路:(BFS)

返回的是vector的数据类型,定义一个返回类型result;
若根节点为空,返回空(result)为空。
定义一个队列(先进先出)q,首先将根节点入队,pNode用来表示队首,result存入队首的值,将pNode从队首弹出,若队首还存在左右孩子节点,则按顺序入队q,每次都将队首的值存入result中,若其还有左右孩子节点,则按顺序入队,广度优先遍历(BFS)的思想。

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    vector<int> PrintFromTopToBottom(TreeNode* root) {
        vector<int> result;
        if(!root) 
            return result;
        
        queue<TreeNode*> q;
        q.push(root);
        
        while(!q.empty())
        {
            TreeNode* pNode = q.front();
            result.push_back(pNode->val);
            q.pop();
            if(pNode->left)
                q.push(pNode->left);
            if(pNode->right)
                q.push(pNode->right);
        }
        return result;
    }
};

面试题32:把二叉树打印成多行

《剑指Offer(第2版)》读书笔记_第36张图片
思路:

这题的思路和上题差不多,不同的是处理如何换行的问题。
这里通过一个for循环,len表示二叉树每行的节点个数,来打印每行的节点,因为i是从0开始的,终止条件是i == len-1。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
        vector<vector<int> > Print(TreeNode* pRoot) {
            vector<vector<int>> result;
            if(!pRoot) return result;
            
            queue<TreeNode*> q;
            q.push(pRoot);
            while(!q.empty())
            {
                int len = q.size();
                vector<int> res;
                for(int i=0; i<len; i++)
                {
                    TreeNode* pNode = q.front();
                    q.pop();
                    res.push_back(pNode->val);
                    
                    if(i == len-1)
                        result.push_back(res);
                    if(pNode->left)
                        q.push(pNode->left);
                    if(pNode->right)
                        q.push(pNode->right);
                }                
            }            
            return result;
        }
    
};

面试题32:按之字形顺序打印二叉树

在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第37张图片
思路:

这题和上题分行打印的思路相同,不过要增加一个标志位flag变量,用来表示当前行是从左向右打印还是从右向左打印,当flag=true时,从左向右打印,对flag取反flag =! flag,当flag=false时,从右向左打印,通过reverse()函数将容器中的元素进行颠倒。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    vector<vector<int> > Print(TreeNode* pRoot) {
        vector<vector<int>> result;
        if(!pRoot) return result;
        
        queue<TreeNode*> q;
        q.push(pRoot);
        bool flag = true;
        
        while(!q.empty())
        {
            int len = q.size();
            vector<int> res;

            for(int i=0; i<len; i++)
            {
                TreeNode* pNode = q.front();
                q.pop();
                res.push_back(pNode->val);
                if(i == len-1 && flag)
                {
                    result.push_back(res);
                    flag =! flag;
                }
                else if(i == len-1 && !flag)
                {
                    reverse(res.begin(), res.end());
                    result.push_back(res);
                    flag =! flag;
                }
                        
                if(pNode->left)
                    q.push(pNode->left);
                if(pNode->right)
                    q.push(pNode->right);
            }              
        }
        return result;
    }
};

面试题33:二叉搜索树的后序遍历序列

在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第38张图片
思路:(DFS)

①后序遍历特点:左右根
②二叉搜索的特点(题目中说任意两个数字都互不相同):左孩子 < 父节点 < 右孩子
③输入数组的最后一个元素是根节点
④通过左右子树与根节点进行比较
程序思路举例
输入:[4,8,6,12,16,14,10]
return DFS(sequence, 0, 6)
left = 0, right = 6
father = 10
左右子树的分界,div初始为left = 0,
跳出while循环后div = 3,sequence[div] = 12
4、8、6是左子树,12、16、14是右子树,
对左右子树分别进行判断,左子树小于father,右子树大于father。
再对左右子树的子节点分别进行递归判断。

class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        if(sequence.size() == 0) 
            return false;
        if(sequence.size() == 1)
            return true;
        return DFS(sequence,0,sequence.size()-1);
    }
    
    bool DFS(vector<int> sequence,int left,int right)
    {
        //终止条件
        if(left >= right)
            return true;
        
        int father = sequence[right]; //父节点
        //左子树都小于父节点
        int div = left; //定义左右孩子的分界
        //当div跳出while循环之后,所指的是右子树第一个位置
        while(div < right && sequence[div] < father)
            div++;
        //对右子树进行遍历判断,是不是都大于父节点,若小于,则返回false
        for(int i=div; i<right; i++)
        {
            if(sequence[i] < father)
                return false;
        }
        return DFS(sequence,left,div-1) && DFS(sequence,div,right-1);
    }
};

面试题34:二叉树中和为某一值的路径

图文详解两种算法:深度优先遍历(DFS)和广度优先遍历(BFS)

深度优先遍历(DFS)

主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。

广度优先遍历(BFS)

指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。
在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第39张图片
思路:

题目中返回的是vector类型,所以定义vector result;来存储返回值,vector path;用来存储每条符合的路径。
题目中说路径一直要到达叶节点才算完整的路径。
每增加一个节点的值,expectNumber就减去增加的节点的值,递归的终止条件是:sum为0且为叶节点。
若到达了叶节点之后,不满足整个路径之和等于expectNumber,则弹出pop_back()路径path的最后一个节点,表示再换一条路径继续走。

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
public:
    vector<vector<int> > FindPath(TreeNode* root,int expectNumber) {
        DFS(root,expectNumber);
        return result;
    }
    
    void DFS(TreeNode* root,int expectNumber)
    {
        if(!root) return;
        expectNumber -= root->val;
        path.push_back(root->val);
        
        //递归终止条件:sum = 0且到了叶节点
        if(expectNumber == 0 && !root->left && !root->right)
            result.push_back(path);
        
        DFS(root->left,expectNumber);
        DFS(root->right,expectNumber);
        path.pop_back();
    }
};

4.4 分解让复杂问题简单化

面试题35:复杂链表的复制

《剑指Offer(第2版)》读书笔记_第40张图片
《剑指Offer(第2版)》读书笔记_第41张图片
《剑指Offer(第2版)》读书笔记_第42张图片
思路:

书上这题提供了三种思路:
一.
第一步复制原始链表上的每个节点,用next连接起来;第二步设置每个节点的random指针,对于复制链表一个含有n个节点的链表,由于定位每个节点的random都需要从链表头节点开始经过O(n)步才能找到,因此这种方法时间复杂度为O(n2),空间复杂度为O(1)。
二.
基于方法一在random上面的时间花费,通过哈希表开辟辅助空间,第一步仍然是复制原始链表上的每个节点N创建N’,然后把这些创建出来的节点用next链接起来,同时把的配对信息放到一个哈希表中,这样第二步复制的链表就可以根据配对信息完成random指针,由于有了哈希表,只需要用O(1)的时间根据S找到S‘,因此这种方法时间复杂度为O(n),空间复杂度为O(n)。
三.(推荐)(看书上的附图更直观)
程序思路:
第一步:根据原始链表的每个节点N创建对应的N’,这一次,把N‘链接在N的后面。
第二步:设置复制出来的节点的random。原链表上的节点N的random指向S,则对应的复制节点N‘的random是S’是S的下一个节点。
pNode->next->random = pNode->random->next;
第三步:
定义一个虚拟节点dummy,最后返回的是复制链表的头节点即dummy->next,设一个节点cur=dummy,用cur节点来将复制的节点链接起来,pNode为头节点A,pNode->next表示复制链表的头节点A’,则cur->next表示的就是复制链表的头节点A’,cur节点每次都往后移动一格,原链表是一个节点间隔一个节点的,所以pNode->next = pNode->next->next;

/*
struct RandomListNode {
    int label;
    struct RandomListNode *next, *random;
    RandomListNode(int x) :
            label(x), next(NULL), random(NULL) {
    }
};
*/
class Solution {
public:
    RandomListNode* Clone(RandomListNode* pHead) {
        //第一步:复制每个节点
        //在两个节点之间新建一个节点,复制前驱
        for(RandomListNode* pNode=pHead; pNode; )
        {
            RandomListNode* pCloned = new RandomListNode(-1);
            pCloned->label = pNode->label;
            pCloned->random = nullptr;
            //在pNode和pNode->next之间插入pCloned
            RandomListNode* pTemp = pNode->next;
            pNode->next = pCloned;
            pCloned->next = pTemp;
            pNode = pTemp; //为了将循环继续,将pNode的下一个节点赋值给pNode
        }
        //第二步:原链表上的节点N的random指向S,则对应的复制节点N‘的random指向S的下一个节点
        //pNode->next->random = pNode->random->next;
        for(RandomListNode* pNode=pHead; pNode; pNode = pNode->next->next)
        {
            if(pNode->random)
                pNode->next->random = pNode->random->next;
        }
        //第三步:将得到的整个链表拆分成两个链表,奇数位置上的节点组成原始链表,偶数位置组成复制出来的链表
        RandomListNode* dummy = new RandomListNode(-1);
        RandomListNode* cur = dummy;
        for(RandomListNode* pNode=pHead; pNode; pNode = pNode->next)
        {
            cur->next = pNode->next; //节点A
            cur = cur->next;
            pNode->next = pNode->next->next;//原链表每次跳两个
        }
        return dummy->next;
    }
};

面试题36:二叉搜索树与双向链表

《剑指Offer(第2版)》读书笔记_第43张图片
《剑指Offer(第2版)》读书笔记_第44张图片
《剑指Offer(第2版)》读书笔记_第45张图片
思路:

采用书上的思路,这题暂时还没搞懂!
或者不使用书上的思路,即通过中序遍历将节点存在表里面,再链接起来

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    TreeNode *pre=NULL, *head=NULL;
    TreeNode* Convert(TreeNode* pRootOfTree) {
        if(pRootOfTree == NULL) return NULL;
        /*二叉搜索树:左<根<右
        例:按层遍历的一颗二叉搜索树:4,2,5,1,3
        排序的双向列表:从左到右 1——>2——>3——>4——>5
                       从右到左 5——>4——>3——>2——>1
        1.初始化:pre=NULL, head=NULL,cur=pRootOfTree
        2.中序遍历(升序序列):左根右
            判断:if pre==NULL,head=cur
                 if pre!=NULL,pre->right=cur、cur->left=pre
                 if cur==NULL,pre->right=head、head->left=pre
            终止条件:cur==NULL
        */
        dfs(pRootOfTree); //此时cur==NULL
        return head; //返回头节点
    }
    
    void dfs(TreeNode* cur)
    {
        if(cur == NULL) return;
        //1.遍历左子树
        dfs(cur->left);
        //2.遍历根节点
        if(pre != NULL) 
            pre->right=cur;
        else 
            head=cur;
        cur->left=pre;
        pre=cur;
        //3.遍历右子树
        dfs(cur->right);
    }
};

面试题37:序列化二叉树

《剑指Offer(第2版)》读书笔记_第46张图片
《剑指Offer(第2版)》读书笔记_第47张图片

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    //序列化二叉树:传入一个二叉树,返回一个字符数组
    char* Serialize(TreeNode *root) 
    {    
        string result;
        dfs1(root,result);
        char* p = new char[result.size()+1]; //因为有\0所以+1
        strcpy(p, result.c_str()); //将result.c_str()复制给p
        return p;
    }
    
    void dfs1(TreeNode* root, string &result)
    {
        //这棵树如果遍历到空节点,就结束了
        if(!root) 
        {   //#
            result += "#,";
            return;
        }
        //下面遍历非空节点
        result += to_string(root->val) + ",";
        dfs1(root->left, result);
        dfs1(root->right, result);
    }
    
    //反序列化二叉树:传入一个字符数组,返回一个二叉树
    TreeNode* Deserialize(char *str) 
    {
        int idx = 0;
        return dfs2(str,idx);
    }
    
    TreeNode* dfs2(char* str, int &idx)
    {
        //确定长度,比如23长度为2,3长度为1
        int len = idx;
        while(str[len] != ',')
            len++;
        //对于空节点
        if(str[idx] == '#')
        {
            idx = len+1;
            return NULL;
        }
        //对于非空节点,计算数值
        int num = 0;
        //考虑符号+ -
        int sign = 1;
        if(idx < len && str[idx] == '-')
        {
            sign = -1;
            idx++;
        }
        for(int i=idx; i<len; i++)
            num = num*10 + str[i] - '0';
        num *= sign;
        //idx走到下一个数字
        idx = len+1;
        
        TreeNode* root = new TreeNode(num);
        root->left = dfs2(str, idx);
        root->right = dfs2(str, idx);
        
        return root;
    }
};

面试题38:字符串的排序

《剑指Offer(第2版)》读书笔记_第48张图片

示例1
输入:“ab”
返回值:[“ab”,“ba”]
示例2
输入:“abc”
返回值:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

思路:(动态规划)(全排列)

这题的本意是考察全排列的知识,但忽略了若输入的字符串中有重复的字符的情况,比如输入"aab",输出应该是"aab",“aba”,“baa”,但运行本题的程序代码输出的确是"aab"、“aba”、“aab”、“aba”、“baa”、“baa”,所以要对输出结果进行去重的处理。
(我看了牛客网前几名的代码,也没有进行去重处理,但也通过了,可能测试用例里面没有输入aab这种吧)
程序思路:
关于全排列,可以参考这个视频:https://www.bilibili.com/video/av9830088
大概思路参考书上的思路。递归过程,递归的终止条件是当只剩下一个数进行全排列时,递归结束。
先通过全排列把所有的结果列出来,然后字典序通过sort( )函数实现。

class Solution {
public:
    vector<string> result;
    vector<string> Permutation(string str) {
        if(str == "") return result;
        Per(str,0,str.size()-1);
        //题目要求按字典顺序
        sort(result.begin(),result.end());
        return result;
    }
    
    void Per(string str, int head, int tail)
    {
        //递归终止条件:如果头节点和尾节点相遇,即只剩下一个数在进行全排列
        if(head == tail)
            ResultPush(str,tail);
        else
        {
            for(int i=head; i<=tail; i++)
            {
                //首先将每个数都依次放到首位
                swap(str[head],str[i]);
                //对剩下的数进行全排列
                Per(str,head+1,tail);
                //全排列之后再把交换的两个数换回来,为下次交换做准备
                swap(str[head],str[i]); 
            }
        }
    }
    
    void ResultPush(string str, int len)
    {
        //全排列之后的是一个个字符,要把他们连接起来作为一个字符串存入到result中
        string temp;
        for(int i=0; i<=len; i++)
            temp += str[i];
        
        //新建一个迭代器,每次存入reuslt之前,先判断是否有重复的
        vector<string>::iterator iter;
        bool isSame = true;
        for(iter = result.begin(); iter != result.end(); iter++)
        {
            if(*iter == temp)
                isSame = false;
        }
        if(isSame)
            result.push_back(temp);
    }
};

第5章 优化时间和空间效率

5.2 时间效率

面试题39:数组中出现次数超过一半的数字

《剑指Offer(第2版)》读书笔记_第49张图片
《剑指Offer(第2版)》读书笔记_第50张图片
思路:

开辟一个辅助空间,用哈希表来记录每个数字出现的次数,引用哈希表的头文件#include

一开始我写的如下这个代码,在牛客网中编译不通过,出错原因non-void function does not return a value in all control paths,也不知道为什么,可我在VS中测试也没问题。

class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int> numbers) {
        int len = numbers.size();
        if (len == 0) 
            return 0;
        unordered_map<int, int> hash;
        for (int i = 0; i < len; i++)
        {
            hash[numbers[i]]++;
            if (hash[numbers[i]] > len / 2)
                return numbers[i];
        }
}
};

《剑指Offer(第2版)》读书笔记_第51张图片

下面这么写就没问题:

class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int> numbers) {
        int res = 0;
        int len =  numbers.size();
        unordered_map<int, int> hash;
        for (int i = 0; i < len; i++)
        {
            hash[numbers[i]]++;
            if (hash[numbers[i]] > len / 2)
            {
                res = numbers[i];
                break;
            }
        }
        return res;
}
};

面试题40:最小的k个数

《剑指Offer(第2版)》读书笔记_第52张图片
思路一:

暴力解法:把整个数组先进行排序,后再输出。
由于需要先把这个数组排序,最快的时间复杂度需要O(nlogn),最慢要O(n2)。

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> res;
        if(k > input.size()) return res;
        sort(input.begin(),input.end());
        for(int i=0; i<k; i++)
            res.push_back(input[i]);
        return res;
    }
};

思路二:

先定义一个辅助容器res用来存放最小的k个数,当res容器的大小 使用该方法的好处:只需要遍历一次原输入数组input,时间复杂度为O(n),保证res容器中始终都是存储着最小的k个数,对于海量数据该方法很有效。
(题目中没说返回的数组中元素要按照从小到大排列,需要的话就加个sort(res.begin(),res.end());)

class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> res;
        if(k > input.size()) return res;
        
        int len = input.size();
        for(int i=0; i<len; i++)
        {
            if(res.size() < k)
                res.push_back(input[i]);
            else
            {
                //先找到res中的最大值
                int maxOfres = 0;
                vector<int>::iterator iter;
                for(iter=res.begin(); iter!=res.end(); iter++)
                {
                    if(*iter > maxOfres)
                        maxOfres = *iter;
                }
                //如果input[i]比res中的最大元素小,则删除res中的最大元素,将input[i]添加进去
                if(input[i] < maxOfres)
                {
                    vector<int>::iterator iter;
                    for(iter=res.begin(); iter!=res.end(); iter++)
                    {
                        if(*iter == maxOfres)
                        {
                            res.erase(iter);
                            break;//假设最大的元素不在res末尾,删除之后res的容量大小会减一,再次遍历时会越界报错
                        }                            
                    }
                    res.push_back(input[i]);
                }
            }
        }
        //sort(res.begin(),res.end());
        return res;
    }
};

面试题41:数据流中的中位数

在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第53张图片
思路:(大小根堆)

采用大小根堆的方法
①大小根堆的概念
小根堆:较大的数字中,最小的是根节点;大根堆:较小的数字中,最大的是根节点。
②大小根堆的关系
大根堆.size( ) - 小根堆.size( ) = 1或0,如果数据流中是奇数个,则等1,如果是偶数个,则等于0。
③求中位数
如果数据流中是奇数个,则求大根堆的根节点;如果是偶数个,则求(大根堆的根节点和小根堆根节点)/ 2。
(插入的元素都放到大根堆中,要考虑一些特殊情况)

class Solution {
public:
    priority_queue<int> maxheap;
    priority_queue<int,vector<int>,greater<int>> minheap;
    void Insert(int num) {
        maxheap.push(num); //插入的元素都放到大根堆中
        
        //大根堆.size() - 小根堆.size() = 1或0
        if(maxheap.size() - minheap.size() > 1)
        {
            //从大根堆中拿最大的元素到小根堆中
            minheap.push(maxheap.top());
            maxheap.pop();
        }
        
        //如果大根堆中插入的元素都比较大,即大根堆的根节点>小根堆的根节点,交换
        while(minheap.size() && maxheap.top() > minheap.top())
        {
            int max = maxheap.top();
            int min = minheap.top();
            maxheap.pop();
            minheap.pop();
            maxheap.push(min);
            minheap.push(max);
        }
    }

    double GetMedian() { 
        //如果输入的数据流为奇数
        if((maxheap.size() + minheap.size()) % 2 == 1)
            return maxheap.top();
        else
            return (maxheap.top() + minheap.top()) / 2.0;
    }
};

面试题42:连续子数组的最大和

(面试高频题)
《剑指Offer(第2版)》读书笔记_第54张图片

思路:

若即将相加的是一个负数,则肯定会比原来的和更小,所以s=0,表示跳过这个负数,从下一个数开始,但之前最大的和s记下来。
假设输入数组:10,-11,3,4
则 s: 10, 0 , 10,10
则结果result:10, 10 ,10 ,10

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        int res = INT_MIN;
        int s = 0;
        
        for(int i=0; i<array.size(); i++)
        {
            if(s < 0)  //若s小于0,则置为0,表示前面的都作废,从下一个数重新开始计算和
                s = 0;
            s += array[i];
            res = max(s,res);
        }
        return res;
    }
};

面试题43:1~n整数中1出现的次数

《剑指Offer(第2版)》读书笔记_第55张图片
思路一:

暴力解法,累加1~n中每个整数1出现的次数,每次通过对10求余数来判断整数的个位是不是1。

class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n) {
        int res = 0;
        for(int i=1 ;i<=n; i++)
            res += NumOf1(i);
        return res;
    }
    
    int NumOf1(int n)
    {
        int num = 0;
        while(n)
        {
            if(n%10 == 1)
                num++;
            n /= 10;
        }
        return num;
    }
};

思路二:
https://www.bilibili.com/video/av91425312

采用思路二这种方法时间复杂度O(log10n),原先暴力解的时间复杂度是O(nlogn),当n比较大的时候,一般会超时。
这种类别的题目,如果直观求解不行的话,那么通常是进行找规律,转化成一个数学问题。
在分析之前,首先需要知道一个规律
从 1 至 10,在它们的个位数中,数字1出现了 1 次。
从 1 至 100,在它们的十位数中,数字1出现了 10 次。
从 1 至 1000,在它们的百位数中,数字1出现了 100 次。
依此类推,从 1 至 10i,在它们右数第二位中,数字1出现了10 ^ (i - 1)次。
《剑指Offer(第2版)》读书笔记_第56张图片
举例:
在上图中,计算数字abcdef(123456)中万位出现1的次数。
①左边两位是00——ab-1,右边是def,此时从 1 至 10000,在它们的千位数中,数字1出现的次数为:ab×1000;
②左边是ab,如果c=0,表示在它们的千位数中已经为0,就不可能出现1的次数了;如果c=1,表示它们的千位数就是1×××,范围是000——def,则数字1出现的次数为:def+1;如果c>1,表示千位至少是2×××,后面三位数就随便取了,范围是000——999,则数字1出现的次数为:1000。
总结:
把一个数字拆分成:left i right
每一位都要进行上面四个步骤进行计算,我用left表示左边部分ab,i表示当前在第几位c,right表示右边部分def。则把每一个数字拆分成:left i right。
程序思路:
首先对边界进行处理,如果n为0,则直接返回0;
为了方便将数字用left i right进行表示,用一个vector容器来存储每位数,注意存入的是从低位到高位的数字,遍历容器的时候要从容器尾部开始(正常十进制数字从左到右是高位到低位,存入容器后从左到右是低位到高位)。
然后假设左边是数字1、2,则left需要表示出来,将1、2转化成12,right是将4、5、6转化成456,在算右边的时候,算下x,x表示当前所在的位数,比如当前所在是千位,即是10×10×10=1000。
最后根据上图中总结的规律,对于两种情况分别进行讨论。

class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n) {
        if (!n) return 0;

        vector<int> num;
        while (n) //将输入的数的每位数字添加到容器中,如输入123456,则容器中:6,5,4,3,2,1
        {
            num.push_back(n % 10);
            n /= 10;
        }

        int res = 0;
        for (int i = num.size() - 1; i >= 0; i--) //因为容器中的位数反过来了,所以从size()-1开始
        {
            int left = 0, right = 0, x = 1;
            //高位到低位 left
            for (int j = num.size() - 1; j > i; j--)
                left = left * 10 + num[j];
            //计算低位 right
            for (int k = i - 1; k >= 0; k--)
            {
                right = right * 10 + num[k];
                x *= 10;
            }
            //第一种情况
            res += left * x;
            //第二种情况
            if (num[i] == 1) res += right + 1;
            else if (num[i] > 1) res += x;
        }
        return res;
    }
};

面试题44:数字序列中某一位的数字

题目:数字以0123456789101112131415…的格式序列化到一个字符序列中,在这个序列中,第5位(从0开始计数)是5,第13位是1,第19位是4,等等。请写一个函数,求任意第n位对应的数字。

思路:https://www.bilibili.com/video/BV1zK4y1r7tn?p=45&spm_id_from=pageDriver

int digitAtIndex(int n) 
{
    if (!n) return 0;

    //1.确定n是在哪一个数字中,这个数字是几位数
    //找规律:1~9      9个数
    //       10~99    90个数
    //       100~999  900个数
    //i表示几位数,s表示有多少个数,base表示十进制
    long long i = 1, s = 9, base = 1;
    while (n - i * s > 0)
    {      
        n -= i * s;
        i++;
        s *= 10;
        base *= 10;
    }

    //2.确定是几位数的第几个数字
    //比如2位数的第9位,10 11 12 13 14 15 16 17 18
    //(n /i)——向上取整,(n+i-1)/i——向下取整
    int number = base + (n + i - 1) / i - 1; //表示几位数的第几个数字

    //3.确定该数字,锁定具体的哪个位,个十百千万...
    //这里 n%i确定
    int bit = n % i ? n % i : i;
    for (int j = 0; j < i - bit; j++)
        number /= 10;
    return number % 10;
}

面试题45:把数组排成最小的数

在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第57张图片
思路:

若直接对输入数组numbers进行sort()排序,从小到大排序,会发现,若3和32,3<32,会排序成332,但323是小于332的,所以需要对sort()的排序规则进行重新定义。
(在牛客网中,提交需要在函数cmp()前面加上static,否则会报错,我在VS中没加static正确运行。不是很懂什么原因。)
在函数名前面加上static变成静态函数,好处:
<1> 静态函数不能被其他文件所用。
<2> 其他文件中可以定义相同名字的函数,不会发生冲突。
<3> 静态函数会被自动分配在一个一直使用的存储区,直到退出应用程序实例,避免了调用函数时压栈出栈,速度快很多。
如果函数cmp返回结果为False, 那么函数就会将他们互换位置;如果函数cmp返回结果为True,就会保持原来位置不变。

//将整数int类型转化为字符串string类型,加头文件
#include 
class Solution {
public:
    static bool cmp(int a,int b)
    {
        string as = to_string(a),bs = to_string(b);
        return as+bs < bs+as;
    }
    
    string PrintMinNumber(vector<int> numbers) {
        sort(numbers.begin(),numbers.end(),cmp);
        string res;
        for(auto x : numbers)
            res += to_string(x);
        return res;
    }    
};

面试题46:把数字翻译成字符串

题目:给定一个数字,我们按照如下规则把它翻译为字符串:0翻译成“a”,1翻译成“b”,…,11翻译成“l”,…,25翻译成“z”。一个数字可能有多个翻译。例如,12258有5种不同的翻译,分别是“bccfi”、“bwfi”、“bczi”、“mcfi”和“mzi”。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

思路:(DP)
https://blog.csdn.net/L_smartworld/article/details/106374316

动态规划题目思路:
1.确定dp数组以及下标的函数
dp[i]:表示输入数字i时,有dp[i]种不同的翻译方法。
2.确定递推公式
从dp[i]的定义来看,dp[i]可以有两个方向推出来。dp[i]位置的方法由前面一位数字dp[i-1]位置推导出来,dp[i] = dp[i-1],如果前面两位数字在10到25之间,则dp[i-2]也可以影响到dp[i]位置的方法。则dp[i] = dp[i-1] + dp[i-2]。
3.dp数组如何初始化
dp[0] = 0表示输入是0,只会有一种翻译方法;dp[1] = 1表示输入一个数字,肯定在0—9之间,只会有一种翻译方法。
4.确定遍历顺序
由递归公式dp[i] = dp[i-1] + dp[i-2]知,遍历顺序一定是从前向后遍历的。
(注意这里的dp[0]是有效的)

int translateNum(int num) 
{
    if (num == 0)
        return 1;

    string strs = to_string(num);

    vector<int> dp(strs.size() + 1, 0);
    dp[0] = 1;
    dp[1] = 1;
    strs = '0' + strs;

    for (int i = 2; i < strs.size(); i++)
    {
        int temp = (strs[i - 1] - '0') * 10 + strs[i] - '0';

        if (temp >= 10 && temp <= 25)
        {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        else {
            dp[i] = dp[i - 1];
        }
    }
    return dp[strs.size() - 1];
}

可以发现,只需要维护两个数值就可以了,不需要记录整个序列。

int GetTranslationNum(int num)
{
    if (num < 0)
        return 0;
    string stred = to_string(num);  // C++数字转字符串方法
    int prepredpi = 1, predpi = 1, dpi;
    if (stred.size() == 1)
        return 1;
    for (int i = stred.size() - 2; i >= 0; i--)
    {  // 从右向左翻译
        string preSubstr = stred.substr(i, 2);  // substr():获取字符串从i位开始的长度为2的字符串
        if (preSubstr >= "10" && preSubstr <= "25")
            dpi = predpi + prepredpi;
        else
            dpi = predpi;
        prepredpi = predpi;
        predpi = dpi;
    }
    return dpi;
}

牛客网:JZ49 把字符串转换成整数

《剑指Offer(第2版)》读书笔记_第58张图片
《剑指Offer(第2版)》读书笔记_第59张图片
思路:

需要对输入进行判断,“数字"和”-/+数字"都是有效的,其余的都无效返回0。
对str的第一位进行判断,设置标志位sign用来判断正负号,若第一位数字为0,返回0,0123这样的也返回0。
遍历字符串从第二位开始,只有在0~9之间的为正常值且乘上相应的位数,否则不满足返回0。

class Solution {
public:
    int StrToInt(string str) {
        long long num = 0;
        int sign = 1;
        int len = str.size();
        if (len == 0) return num; //若输入为空,返回0

        if ((str[0] == '-' || str[0] == '+') && len == 1)
            return num; //若第一位是-+号,后面就没了,返回0
        
        //先处理字符串第一位
        if (str[0] == '-')
            sign = -1;
        else if (str[0] == '0')
            return num;
        else if (str[0] > '0' && str[0] <= '9')
        {
            int base = 1;
            for (int i = 0; i < len - 1; i++)
                base *= 10;
            num += (str[0] - '0') * base;
        }
        
        //从第二位开始
        for (int i = 1; i < len; i++)
        {
            if (str[i] >= '0' && str[i] <= '9')
            {
                int base = 1;
                for (int j = 0; j < len - i - 1; j++)
                    base *= 10;
                num += (str[i] - '0') * base;
            }
            else
                return 0;
        }
        num = sign * num;

        return num;
    }
};

面试题47:礼物的最大价值

《剑指Offer(第2版)》读书笔记_第60张图片
思路:(DP)

关于x,y在范围rows和cols的问题,我老和Opencv中的rows和cols搞混。
int rows = grid.size();得到的是二维数组grid的行数,int cols = grid[0].size();得到的是二维数组的列数。按理说,横坐标x,纵坐标y,则x的范围应该是[0,cols],因为列数决定了x的大小,但在数组中grid[x][y]表示的是第x行y列的数,所以x的范围是[0,rows],同理y的范围是[0,cols]。
程序思路:
从左上角grid[0][0]开始,最终走到grid[rows-1][cols-1]结束。可以发现,一共走的步数是(rows+cols-1)步。
第i的礼物最大值由i-1步的礼物最大值来决定,是动态规划问题
动态规划问题解题步骤:
1.确定dp数组以及下标的函数
dp[i]:第i步时,礼物的最大值为dp[i]。
2.确定递推公式
从dp[i]的定义来看,dp[i]第i的礼物最大值由i-1步的礼物最大值dp[i-1]来决定,因为只能往右或者往下移动,所以dp[i] = max((dp[i-1]+grid[x+1][y]),(dp[i-1]+grid[x][y+1]))。
3.dp数组如何初始化
dp[0]=0表示一步都没走的话,礼物的最大值肯定为1,dp[1]=grid[0][0]因为第一步肯定是从左上角开始的,所以是左上角礼物的价值。
4.确定遍历顺序
由递归公式dp[i] = max((dp[i-1]+grid[x+1][y]),(dp[i-1]+grid[x][y+1]))知,遍历顺序一定是从前向后遍历的。
PS:本题除了要考虑动态规划,还要考虑移动是否越界数组的问题,如果已经到了底部,就不能再往下移动,只能往右,如果已经到了二维数组最右边,就不能往右移动,只能往下。)

int getMaxValue(vector<vector<int>>& grid)
{
    if (!grid.size()) return 0;
    int rows = grid.size();
    int cols = grid[0].size();
    vector<int> dp(rows + cols);
    dp[0] = 0;
    dp[1] = grid[0][0];
    int x = 0, y = 0; //起点
    for (int i = 2; i <= rows + cols - 1; i++)
    {
        if (x < rows && y < cols)
        {
            if ((x+1 < rows) && (y+1 < cols) && (grid[x + 1][y] > grid[x][y + 1]))
            {
                dp[i] = dp[i - 1] + grid[x + 1][y];
                x++;
            }             
            else if ((x + 1 < rows) && (y + 1 < cols) && (grid[x + 1][y] < grid[x][y + 1]))
            {
                dp[i] = dp[i - 1] + grid[x][y + 1];
                y++;
            }
            else if ((x + 1 >= rows) && (y + 1 < cols)) //如果已经到了底部,就不能再往下移动,只能往右
            {
                dp[i] = dp[i - 1] + grid[x][y + 1];
                y++;
            }
            else if ((y + 1 >= cols) && (x + 1 < rows)) //如果已经到了二维数组最右边,就不能往右移动,只能往下
            {
                dp[i] = dp[i - 1] + grid[x + 1][y];
                x++;
            }
        }
    }
    return dp[rows + cols - 1];
}

法二:

int getMaxValue(vector<vector<int>>& grid)
{
    int x = grid.size(), y = grid[0].size();
    vector<vector<int>> f(x + 1, vector<int>(y + 1));
    for (int i = 1; i <= x; i++)
        for (int j = 1; j <= y; j++)
            f[i][j] = max(f[i - 1][j], f[i][j - 1]) + grid[i - 1][j - 1];
    return f[x][y];
}

面试题48:最长不含重复字符的子字符串

题目:请从字符串中找出一个最长的不包括重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含’a’~'z’的字符。例如,在字符串"arabcacfr"中,最长的不含重复字符的子字符串是"acfr",长度为4。

思路:(哈希表+双指针)

程序思想:i,j双指针,可以这样想,把str比作一行数组,j从上面指向数组,用来记录当前str的位置,每次都在自增1;i从下面指向数组,若i和j指针之间没有重复元素时,j自增1;若其中有重复字符串了,则i自增1,因此字符串的长度是j-i+1,i自增1就是缩短字符串的长度。
如果单从双指针入手,可以不用哈希表这个数据结构,但每次新遍历一个字符,比如bca,再遍历c时,还需要遍历bca中是否有c,又增加了复杂度,但如果使用了哈希表,因为每个字符对应一个键值对,再新遍历一个字符时,值自增1,若大于1就可以发现有重复的。

class Solution {
public:
    int LongestSubstringWithoutDuplication(string str)
    {
        int res = 0;
        unordered_map<char, int> hash;
        for (int i = 0, j = 0; j < str.size(); j++)
        {
            hash[str[j]]++;
            while (hash[str[j]] > 1)
            {
                hash[str[i]]--;
                i++;
            }
            res = max(res, j - i + 1);
            cout << "res:" << res << endl;
        }
        return res;
    }
};

5.3时间效率与空间效率的平衡

面试题49:丑数

在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第61张图片
思路:

本体采用暴力解法会超时,因为不管是不是丑数都需要计算,根据丑数的定义,丑数应该是另一个丑数乘以2、3或5的结果,所以用一个容器存储丑数,按大小排好。

class Solution {
public:
    int GetUglyNumber_Solution2(int index)
    {
        //因为1,2,3,4,5,6都是丑数,输入几就是第几个丑数是几
        if (index < 7)
            return index;
        vector<int> uglyVector(1, 1); //定义容器的大小为1,且全部初始化为1
        int num_2 = 0, num_3 = 0, num_5 = 0;
        while (--index) //因为1是算丑数的,先减去1;循环n-1次
        {
            int ugly_num = min(min(uglyVector[num_2] * 2, uglyVector[num_3] * 3), uglyVector[num_5] * 5);
            uglyVector.push_back(ugly_num);
            if (ugly_num == uglyVector[num_2] * 2)
                num_2++;
            if (ugly_num == uglyVector[num_3] * 3)
                num_3++;
            if (ugly_num == uglyVector[num_5] * 5)
                num_5++;
        }
        return uglyVector.back();
    }
};

面试题50:字符串中第一个只出现一次的字符

《剑指Offer(第2版)》读书笔记_第62张图片
思路:

建一个hash表用于存储每个字符出现的次数,遍历这个哈希表,第一个出现次数为1的就是要找的那个字符

class Solution {
public:
    int FirstNotRepeatingChar(string str) {
        unordered_map<char,int> hash;
        int len = str.size();
        for(int i=0; i<len; i++)
            hash[str[i]]++;
        for(int i=0; i<len; i++)
        {
            if(hash[str[i]] == 1)
                return i;
        }
        return -1;
    }
};

面试题50:字符流中第一个只出现一次的字符

《剑指Offer(第2版)》读书笔记_第63张图片
《剑指Offer(第2版)》读书笔记_第64张图片
思路:
一般面试中遇到第一次出现的××,一般都用哈希表。

函数FirstAppearingOnce()的功能是返回字符流中第一个不重复的字符,通过建立一个队列,每次返回队列的队首表示第一个不重复的字符,若队列为空,则返回’#’。则要保证每次Insert(char ch)的字符的哈希值为1,若大于1,就要对队列队首进行判断,其队首是否哈希值为1,因为返回的是队首所以只关心队首,每次都要保证队首只出现了一次即可,后面不用管。输入的字符流也要考虑是否是字符,而不是空字符。

class Solution
{
public:
    unordered_map<char,int> hash; //记录每个字符出现的次数
    queue<char> res;
  //Insert one char from stringstream
    void Insert(char ch) {
         if(ch == ' ')
             return;
        hash[ch]++;
        if(hash[ch] > 1)
        {
            while(res.size() && hash[res.front()] > 1)
                res.pop();
        }
        else
            res.push(ch);
    }
  //return the first appearence once char in current stringstream
    char FirstAppearingOnce() {
        if(!res.size())
            return '#';
        return res.front();
    }
};

面试题51:数组中的逆序对

《剑指Offer(第2版)》读书笔记_第65张图片
思路:(归并排序、分治思想)

https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/solution/shu-zu-zhong-de-ni-xu-dui-by-leetcode-solution/
举例:{ 7, 5, 6, 4}
归并的思想,{7,5,6,4}先分成{7,5},{6,4},再分成{7},{5},{6},{4}。先判断此层的逆序对{7}和{5}组成逆序对,{6}和{4}组成逆序对,则res = 2;排序后往上一层走,{5,7},{4,6},先考虑5和4谁在第一个,因为4小于5,且7大于5,则5和4、7和4都组成逆序对,res = 2+2=4,6比7小,则7和6组成逆序对,则res = 2+2+1=5,此时数组已经归并排序完毕。

class Solution {
public:
    int InversePairs(vector<int> data) {
        if(!data.size()) return 0;
        int len = data.size();
        return merge(data,0,len-1);
    }
    
    //不能写成vector data
    int merge(vector<int>& data, int data_l, int data_r)
    {
        //程序总体思想
        //假设数组  [............]
        //分成两部分[..i..mid][mid+1..j..]
        //如果i位置的元素已经大于j位置的元素,由于数组有序,那么i之后的元素都是大于j位置的元素。因此逆序对的总个数为:mid-i+1

        //归并排序的终止条件
        if (data_l >= data_r)
            return 0;
        int mid = data_l + (data_r - data_l) / 2;
        int res = merge(data, data_l, mid) + merge(data, mid + 1, data_r);

        //归并的过程
        int i = data_l, j = mid + 1;
        vector<int> mergeArr; //定义一个临时数组,用来记录当前归并之后最终有序的数组
        while (i <= mid && j <= data_r)
        {
            if (data[i] > data[j]) //构成逆序对
            {
                mergeArr.push_back(data[j]);
                (res += mid - i + 1) %= 1000000007;
                j++;
            }
            else
            {
                mergeArr.push_back(data[i]);
                i++;
            }
        }

        //按照上面的比较,肯定会有一部分区间还没有比较结束,则直接放入temp临时容器
        while (i <= mid)
        {
            mergeArr.push_back(data[i]);
            i++;
        }
        while (j <= data_r)
        {
            mergeArr.push_back(data[j]);
            j++;
        }

        i = data_l;
        for (auto x : mergeArr)
            data[i++] = x;

        return res;
    }
};

面试题52:两个链表的第一个公共节点

《剑指Offer(第2版)》读书笔记_第66张图片
思路一:(双指针)
《剑指Offer(第2版)》读书笔记_第67张图片
《剑指Offer(第2版)》读书笔记_第68张图片

时间复杂度:O(m+n),其中m和n是分别是链表pHead1和pHead2的长度。需要遍历两个链表各一次,两个指针同时遍历两个链表,每个指针遍历两个链表各一次。
空间复杂度:O(1)

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        if (pHead1 == nullptr || pHead2 == nullptr) {
            return nullptr;
        }
        ListNode *pA = pHead1, *pB = pHead2;
        while (pA != pB) //若pA=pB=nullptr,返回空
        {
            pA = pA == nullptr ? pHead2 : pA->next;
            pB = pB == nullptr ? pHead1 : pB->next;
        }
        return pA;
    }
};

思路二:(使用栈)

如果两个链表有公共节点,那么公共节点出现在两个链表的尾部。可以通过两个辅助栈,分别把两个链表的节点放入两个栈里,这样两个链表的尾节点就位于两个栈的栈顶,接下来比较两个栈顶的节点是否相同。如果相同,则把栈顶弹出接着比较下一个栈顶,直到找到最后一个相同的节点。
时间复杂度:O(m+n)其中m和n是分别是链表pHead1和pHead2的长度。需要遍历两个链表各一次。两个指针同时遍历两个链表,每个指针遍历两个链表各一次。
空间复杂度:O(m+n),需要两个辅助栈

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) {
        if (pHead1 == nullptr || pHead2 == nullptr)
            return nullptr;
        ListNode* p1 = pHead1;
        ListNode* p2 = pHead2;
        stack<ListNode*> s1;
        stack<ListNode*> s2;
        while (p1 != nullptr)
        {
            s1.push(p1);
            p1 = p1->next;
        }
        while (p2 != nullptr)
        {
            s2.push(p2);
            p2 = p2->next;
        }

        if (s1.top() != s2.top())
            return nullptr;
        ListNode* pNode = nullptr;
        while (!s1.empty() && !s2.empty() && s1.top() == s2.top())
        {
            pNode = s1.top();
            s1.pop();
            s2.pop();
        }
        return pNode;
    }
};

第6章 面试中的各项能力

6.3 知识迁移能力

二分查找算法可以用来在排序数组中查找一个数字

面试题53:数字在升序数组中出现的次数

讲解:
https://www.bilibili.com/video/BV1UV411H7ek?t=26
《剑指Offer(第2版)》读书笔记_第69张图片
思路:(二分法)

因为数组是升序的,一个数字出现的次数在数组中肯定是一个区间,找出这个区间的左端点left和右端点right,通过right-left+1得到这个数字出现的次数。
左端点left,在left左边的数都比left(k)小,在left右边的数>=left(k);
右端点right,在right左边的数<=right(k),在right右边的数>right(k)。

class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) {
        if(data.size() == 0) return 0;       
        //找左端点
        int l = 0, r = data.size()-1;
        while(l < r)
        {
            int mid = l + (r - l)/2;
            if(data[mid] < k)
                l = mid+1;
            else
                r = mid;
        }
        //如果没有找到左端点,表示不不存在这个数,返回0
        if(data[l] != k) return 0;
        int left = l;
        //找右端点
        l = 0, r = data.size()-1;
        while(l < r)
        {
            int mid = l + (r - l + 1)/2; 
            //这里不写成mid = l + (r - l)/2;因为r=mid-1,考虑边界问题
            if(data[mid] > k)
                r = mid-1;
            else
                l = mid;
        }
        int right = r;
        return right-left+1;
    }
};

面试题53:0~n-1中缺失的数字

题目:一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0-n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
样例
输入:[0, 1, 2, 4]
输出:3
输入:[1, 2, 3, 4]
输出:0
输入:[0, 1, 2, 3]
输出:4

思路:(二分)

若使用从头到尾遍历数组的方法,找到第一个值与下标不相等的数字返回,则时间复杂度是O(n)。
采用二分法,时间复杂度是O(logn)
首先对输入值进行判断,如输入数组为空,则返回-1;
先考虑两种特殊情况,缺失的数字分别在首和尾。因为n个数中有且只有一个数字不在该数组中,若数组中第一个数不是0,则肯定缺失的是0,若数组中最后一个数和数组长度减一相同,则肯定缺失的是数组长度的那个数字。
最后考虑缺失的数字在数组中间的情况,使用二分法,若数组中间的数字nums[mid]与下标mid不同,则说明缺失的数字肯定在左半部分,right = mid;反之若相同,则缺失的数字肯定在右半部分,left = mid+1。

class Solution {
public:
    int getMissingNumber(vector<int> nums)
    {
        int len = nums.size();
        if (len == 0) return -1;

        if (nums[0] != 0) return 0;
        if (nums[len - 1] == len - 1) return len;

        int l = 0, r = len - 1;
        while (l < r)
        {
            int mid = l + (r - l) / 2;
            if (nums[mid] != mid)
                r = mid;
            else
                l = mid + 1;
        }
        return l;
    }
};

面试题53:数组中数值和下标相等的元素

《剑指Offer(第2版)》读书笔记_第70张图片
思路:(二分)

暴力解法:时间复杂度O(n),从头到尾遍历数组,找到数字和下标相等的元素即nums[i] == i的元素。
采用二分法,时间复杂度是O(logn)

int getMissingNumber(vector<int> nums)
{
    int len = nums.size();
    if (len == 0) return -1;

    int l = 0, r = len - 1;
    while (l < r)
    {
        int mid = l + (r - l) / 2;
        if (nums[mid] == mid)
            return mid;
        if (nums[mid] > mid)
            r = mid - 1;
        else
            l = mid + 1;
    }
    return -l;
}

面试题54:二叉搜索树的第k个结点

《剑指Offer(第2版)》读书笔记_第71张图片
思路:

中序遍历:左根右,从小到大的顺序遍历。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    TreeNode* res = NULL;
    TreeNode* KthNode(TreeNode* pRoot, int k) {
        if(pRoot == nullptr || k == 0) return NULL;
        return Inorder(pRoot,k);
    }

    //中序遍历:左根右
    TreeNode* Inorder(TreeNode* pRoot,int &k)
    {
        if(pRoot->left != nullptr)
            Inorder(pRoot->left, k);
        k--;
        if(k == 0) res = pRoot;
        if(k > 0 && pRoot->right != nullptr)
            Inorder(pRoot->right,k);
        return res;
    }
};

面试题55:二叉树的深度

在这里插入图片描述
《剑指Offer(第2版)》读书笔记_第72张图片
思路:(递归法)

  1. 确定递归函数的参数和返回值:参数就是传⼊树的根节点,返回就返回这棵树的深度,所以返回值为int类型。
  2. 确定递归终止条件:如果为空节点的话,就返回0,表示高度为0。
  3. 确定单层递归的逻辑:先求它的左⼦树的深度,再求的右子树的深度,最后取左右深度最⼤的数值再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度。
/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    int TreeDepth(TreeNode* pRoot) {
        return getDepth(pRoot);
    }
    
    int getDepth(TreeNode* pRoot)
    {
        if(pRoot == nullptr) return 0;
        int leftNum = getDepth(pRoot->left);
        int rightNum = getDepth(pRoot->right);
        return max(leftNum,rightNum) + 1;
    }
};

面试题55:平衡二叉树

《剑指Offer(第2版)》读书笔记_第73张图片
《剑指Offer(第2版)》读书笔记_第74张图片
思路:

分别求出左右子树的高度,然后如果差值abs( )小于等于1,则返回当前二叉树的高度,否则返回-1,-1表示不是平衡二叉树。

class Solution {
public:
    bool IsBalanced_Solution(TreeNode* pRoot) {
        if(getDepth(pRoot) == -1)
            return false;
        else
            return true;
    }
    
    int getDepth(TreeNode* pRoot)
    {
        if(pRoot == nullptr)
            return 0;
        int leftNum = getDepth(pRoot->left);
        if(leftNum == -1) return -1;
        int rightNum = getDepth(pRoot->right);
        if(rightNum == -1) return -1;
        
        int res = 0;
        if(abs(leftNum - rightNum) > 1)
            return -1;
        else
            res = 1 + max(leftNum,rightNum); 
        return res;
    }
};

面试题56:数组中只出现一次的两个数字

《剑指Offer(第2版)》读书笔记_第75张图片
法一:思路:(哈希表)

时间复杂度:O(n);空间复杂度O(n)

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param array int整型vector 
     * @return int整型vector
     */
    vector<int> FindNumsAppearOnce(vector<int>& array) {
        unordered_map<int,int> hash;
        vector<int> res;
        for(int i=0; i<array.size(); i++)
            hash[array[i]]++;
        for(int i=0; i<array.size(); i++)
        {
            if(hash[array[i]] == 1)
                res.push_back(array[i]);
        }            
        return res;
    }
};

法二:思路:(采用异或的方法)

时间复杂度:O(n);空间复杂度O(1)
题目中说其他数字都出现了两次,异或运算的性质:任何一个数字异或它自己都等于0。
程序思路:根据异或的性质:x^x = 0, x^0 = x, x^y = z,遍历原数组进行异或,最后得到只出现一次的两个数字num1和num2的异或值sum。
接下来将num1和num2从sum中拆分出来,因为num1和num2肯定是不相同的两个数,则它们的二进制数也不同,肯定至少有一位不同,0或者1,所以先找到不同的那一位数是第几位,得到k。
之后根据书上的思路,遍历每个元素都左移k个数,假设先得出num1,num1num2num1 = num2即num2 = sum ^ num1。

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param array int整型vector 
     * @return int整型vector
     */
    vector<int> FindNumsAppearOnce(vector<int>& array) {
        vector<int> res;
        int sum = 0;
        int len = array.size();
        for(int i=0; i<len; i++)
            sum ^= array[i];
        
        int k=0;
        while(!((sum>>k) & 1)) 
            k++;
        
        int num1 = 0, num2 = 0;
        for(int j=0; j<len; j++)
        {
            if((array[j]>>k) & 1 == 1)
                num1 ^= array[j];
        }
        num2 = sum^num1;
        
        //题目要求返回的结果中较小的数排在前面  
        if(num1 < num2)
        {
            res.push_back(num1);
            res.push_back(num2);
        }
        else
        {
            res.push_back(num2);
            res.push_back(num1);
        }
        return res;
    }
};

面试题56:数组中唯一只出现一次的数字

题目:在一个数组中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

法一:思路:(哈希表)

时间复杂度:O(n);空间复杂度O(n)

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        unordered_map<int,int> hash;
        int len = nums.size();
        int res = 0;
        for(int i=0; i<len; i++)
            hash[nums[i]]++;
        for(int i=0; i<len; i++)
        {
            if(hash[nums[i]] == 1)
            {
                res = nums[i];
                break;
            }                       
        }
        return res;        
    }
};

法二:

时间复杂度:O(n);空间复杂度O(1)
把数组中所有数字的二进制表示的每一位都加起来。如果某一位的和能被3整除,那么那个只出现一次的数字二进制表示中对应的那一位是0,否则就是1。

int singleNumber(vector<int>& nums) 
{
	int bitSum[32] = { 0 };
	int len = nums.size();
	for (int i = 0; i < len; i++)
	{
		for (int j = 0; j < 32; j++)
		{
			int temp = nums[i] & 1;
			bitSum[j] += temp;
			nums[i] = nums[i] >> 1;
		}
	}

	int res = 0;
	int bit = 1; //二进制
	for (int i = 0; i < 32; i++)
	{
		res += (bitSum[i] % 3) * bit;
		bit = bit << 1;
	}
	return res;
}

面试题57:和为s的两个数字

在这里插入图片描述
思路:(双指针法)

定义两个指针,分别指向数组头部start和尾部end,因为数组是递增的,若start+end = sum,则直接返回这两个数(题目要求若有多对符合的数,返回两个数乘积最小的,因为按照前后两个指针进行遍历的话,先符合的两个数的乘积肯定小于后面符合的两个数的乘积;定义返回结果数组res,先放入start,后放入end,也符合题目要求的输出两个数,小的先输出);若start+end < sum,则要把start往后移动一位;若start+end > sum,则要把end往前移动一位。

class Solution {
public:
    vector<int> FindNumbersWithSum(vector<int> array,int sum) {
        vector<int> res;
        int len = array.size();
        int start = 0;
        int end = len-1;
        while(start < end)
        {
            if((array[start] + array[end]) == sum)
            {
                res.push_back(array[start]);
                res.push_back(array[end]);
                break;
            }
            else if((array[start] + array[end]) < sum)
                start++;
            else if((array[start] + array[end]) > sum)
                end--;
        }
        return res;
    }
};

面试题57:和为s的连续正数序列

《剑指Offer(第2版)》读书笔记_第76张图片
思路:(双指针)

假设存在连续的正整数序列,序列头指针i和序列尾指针j,从1开始,当s小于sum时,指针j往后移动一格,当s==sum且指针j在指针i后面,输出符合条件的正整数序列,若s>sum,则指针i往后移动一格,且之前相加的和s要减去原来的指针i的值。则i从2开始,j再往后寻找。

class Solution {
public:
    vector<vector<int> > FindContinuousSequence(int sum) {
        vector<vector<int>> res;
        for(int i=1,j=1,s=1; i<sum; i++)
        {
            while(s < sum)
            {
                j++;
                s += j;
            }
            if(s == sum && j-i > 0) //如果找到了这样的序列,则放入res中
            {
                vector<int> result;
                for(int k=i; k<=j; k++)
                    result.push_back(k);
                res.push_back(result);
            }
            s -= i;
        }
        return res;
    }
};

面试题58:翻转字符串:翻转单词顺序

《剑指Offer(第2版)》读书笔记_第77张图片

class Solution {
public:
    string ReverseSentence(string str) {
        if(!str.size())
            return "";
        //第一步:翻转句子中所有的字符
        reverse(str.begin(),str.end());
        //第二步:翻转每个单词中字符的顺序
        for(int i=0; i<str.size(); i++)
        {
            int j = i;
            while(str[j] != ' ' && j < str.size())
                j++;
            reverse(str.begin()+i, str.begin()+j); //将i到j之间的字符翻转
            i = j;
        }
        return str;
    }
};

面试题58:翻转字符串:左旋转字符串

《剑指Offer(第2版)》读书笔记_第78张图片
思路:

1.翻转整个字符串
2.翻转前半部分字符串
3.翻转后半部分字符串

class Solution {
public:
    string LeftRotateString(string str, int n) {
        reverse(str.begin(),str.end());
        int len = str.size();
        reverse(str.begin(),str.begin()+len-n);
        reverse(str.begin()+len-n,str.end());
        return str;
    }
};

面试题59:队列的最大值:滑动窗口的最大值

《剑指Offer(第2版)》读书笔记_第79张图片
法一:

暴力解法,扫描每个滑动窗口的所有数字并找出其中最大值,如果滑动窗口的大小为k,对于长度为n的输入数组,总时间复杂度是O(nk)。
对于特殊情况:①窗口大于数组长度的时候,返回空;②窗口大小为0,返回空;③窗口大小为1,返回输入数组

class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size) {
        vector<int> res;
        int len = num.size();
        if(len < size || size == 0) return res;
        if(size == 1) return num;
        for(int i=0; i<=len-size; i++)
        {
            int max = num[i];
            for(int j=i+1; j<i+size; j++)
            {
                if(num[j] > max)
                    max = num[j];
            }
            res.push_back(max);
        }
        return res;
    }
};

法二:

采用双向队列的方法。时间复杂度O(n)。
程序思路:
用一个双向队列q_index来存储输入数组中每个数字的索引,假设窗口大小为3,则需要当索引值i >= 2时才能往结果数组res中输入,因为i是从0开始的,i >= size-1,且输入的是双向队列的头部num[q_index.front()]。
为了保证双向队列的头部所对应的索引的数字永远都是当前滑窗的最大值,需要对这个双向队列进行维护,①队头什么时候出队?当入队的长度已经大于等于窗口的大小时,队头出队,即i-q_index.front() >= size;②在存入一个数字的下标之前,首先要判断队列里已有数字是否小于待存入的数字。如果已有的数字小于待存入的数字,那么这些数字已经不可能是滑动窗口的最大值,因此将它们从队尾删除。(如果待存入的数字小于队尾数字,将直接存入队列中,因为虽然当前最大的数字不是待存入的数字,但当滑窗往后移动时,最大的数字被删除了,则待存入的数字可能成为最大的值)

class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size) {
        vector<int> res;
        int len = num.size();
        if(len == 0 || len < size || size == 0) return res;
        if(size == 1) return num;
        
        deque<int> q_index;
        for(int i=0; i<len; i++)
        {
            //队头什么时候出队?
            //当队列中的元素个数大于等于窗口大小时
            while(!q_index.empty() && i-q_index.front() >= size)
                q_index.pop_front();
            //什么时候pop队尾?
            //如果队列中的数字小于待存入的数字,那么这些数字已经不可能是滑动窗口的最大值,从队列尾部依次删除
            while(!q_index.empty() && num[q_index.back()] <= num[i])
                q_index.pop_back();
            q_index.push_back(i);
            
            //假设滑动窗口大小为3,i从0开始,则当i=2时,就要往res中添加最大值
            if(i >= size-1)
                res.push_back(num[q_index.front()]);
        }
        return res;
    }
};

面试题59:队列的最大值:队列的最大值

《剑指Offer(第2版)》读书笔记_第80张图片
思路:
https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/solution/mian-shi-ti-59-ii-dui-lie-de-zui-da-zhi-by-leetcod/

队列(queue)是push( );双向队列(deque)是push_back( );
需要一个辅助双向队列d,d的队首永远都存放当前队列q的最大值。
max_value():题目中说了,若队列为空,max_value 需要返回 -1;当d不为空时,队列q的最大值就是双向队列d的队首。
push_back(int value):队列q正常push(value);当双向队列d的队尾数字大于等于value时,要d.push_back(value),思路和上一题一样,因为虽然此时的max_value不是value,但当前面的数字都pop_front()之后,value可能成为最大的max_value;反之,当双向队列d的队尾数字小于value时,则要将队尾数字pop_back()掉,因为value添加进队列后,此时队列的max_value就是value。
pop_front():若队列为空,pop_front需要返回 -1;若队列不为空,则返回队列q的队首且q.pop();若q的队首等于d的队首,即删除的队首若也是当前队列的最大值,则双向队列d也需要d.pop_front();
《剑指Offer(第2版)》读书笔记_第81张图片

class MaxQueue {
public:
    queue<int> q; //定义的队列
    deque<int> d; //辅助队列
    MaxQueue() {

    }
    
    int max_value() {
        if(d.empty())
            return -1;
        return d.front();
    }
    
    void push_back(int value) {
        q.push(value);
        while(!d.empty() && d.back() < value)
            d.pop_back();
        d.push_back(value);
    }
    
    int pop_front() {
        if(q.empty())
            return -1;
        int temp = q.front();
        if(temp == d.front())
            d.pop_front();
        q.pop();
        return temp;
    }
};

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue* obj = new MaxQueue();
 * int param_1 = obj->max_value();
 * obj->push_back(value);
 * int param_3 = obj->pop_front();
 */

6.4 抽象建模能力

面试题60:n个骰子的点数

https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/

题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
示例 1:
输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:
输入: 2
输出:
[0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]

思路:(动态规划)

动规五步曲:

  1. 确定dp数组以及下标的含义
    dp[i][j]的定义为:表示n个骰子中前i个骰子点数和为j的情况的次数;
  2. 确定递推公式
    设输入i个骰子的解为dp[i],且点数和为j的情况为dp[i][j]次; 假设已知i-1个骰子的解为dp[i-1],此时添加一枚骰子,求i个骰子的点数和为j的出现的次数dp[i][j]。
    当添加骰子的点数为1时,前i-1个骰子的点数和应为j-1,方可组成点数和j;同理,当此骰子为2时,前i-1个骰子点数应为j-2;以此类推,直至骰子点数为6。将这6种情况的次数相加,即可得到i个骰子点数和为j的情况的次数dp[i][j],递推公式如下:
    在这里插入图片描述
    根据以上分析,得知通过子问题的解dp[i-1]可递推计算出dp[i],而输入一个骰子的解dp[1]已知,因此可以通过解dp[1]依次地推出任意解dp[i];
  3. dp数组如何初始化
    dp[0][0] = 1;
  4. 确定遍历顺序
    根据递推公式知,从前往后遍历;
  5. 举例推导dp数组
    dp[1][1] = dp[0][0] = 0。
class Solution {
public:
    vector<double> dicesProbability(int n) {
        //定义一个二维容器用来存储点数和从n~6*n的次数,因为dp[0][0]不用,所以都各加1
        vector<vector<int>> dp(n+1,vector<int>(6*n+1)); 
        dp[0][0] = 1;
        for(int i=1; i<=n; i++) //前i个骰子
            for(int j=1; j<=6*i; j++) //点数和为j
                for(int k=1; k<=min(j,6); k++) //k为1~6,且保证下面的j-k>=0,保证容器不越界
                    dp[i][j] += dp[i-1][j-k];

        vector<int> times; //表示n个骰子的点数和所有可能的次数
        for(int i=n; i<=6*n; i++)
            times.push_back(dp[n][i]);
        
        int sum = 0; //先求出出现所有可能的值的总次数,即分母
        for(auto x:times)
            sum += x;

        vector<double> res;
        for(auto x:times)
            res.push_back(double(x)/double(sum));
        return res;
    }
};

面试题61:扑克牌中的顺子

《剑指Offer(第2版)》读书笔记_第82张图片
《剑指Offer(第2版)》读书笔记_第83张图片
思路:

程序思路:
首先对输入是否为空进行判断;
然后对numbers排序,目的是把0都移动到numbers前面,用zeroIndex索引来记录numbers中最后一个0的下一个位置,比如0,0,2,4,6,则zeroIndex=2;
对于特殊情况若四张牌都是大小王,即numbers中4个0,则直接返回true;
若除0外,若有任意两张牌相同,则直接返回false;
若最大值-最小值>4,则五张牌肯定不能是顺子,则直接返回false。

class Solution {
public:
    bool IsContinuous( vector<int> numbers ) {
        sort(numbers.begin(),numbers.end());
        int zeroIndex = 0; //记录是0的下一个索引位置
        for(int i=0; i<5; i++)
        {
            if(numbers[i] == 0)
                zeroIndex++;
        }
        //如果四张牌都是大小王,则直接返回true
        if(zeroIndex == 4) return true;
        //除0外,若有任意两张牌相同,则直接返回false
        for(int i=zeroIndex; i<4; i++) //防止i+1容器越界,所以索引最大为3,<4
        {
            if(numbers[i] == numbers[i+1])
                return false;
        }
        //如果最大值-最小值>4,则五张牌肯定不能是顺子,则直接返回false
        if((numbers[4] - numbers[zeroIndex]) > 4)
            return false;
        return true;
    }
};

面试题62:圆圈中最后剩下的数字

《剑指Offer(第2版)》读书笔记_第84张图片
法一:

用环形链表模拟圆圈,思路见书上,由于list本身不是一个环形结构,因此每当迭代器(Iterator)扫描到链表末尾的时候,要记得把迭代器移到链表的头部,这样相当于按照顺序在一个圆圈里遍历了。
时间复杂度:O(mn)
空间复杂度:O(n)

class Solution {
public:
    int LastRemaining_Solution(int n, int m) {
        if(n < 1 || m < 1)
            return -1;
        list<int> arr; 
        for(int i=0; i<n; i++) //建立圆圈中的元素0~n-1
            arr.push_back(i);
        
        list<int>::iterator cur = arr.begin();
        while(arr.size() > 1) //当容器中至少有两个元素时
        {
            for(int i=1; i<m; i++)
            {
                cur++;
                //每当迭代器cur扫描到容器末尾时,就将其移动到链表的头部
                if(cur == arr.end())
                    cur = arr.begin();
            }
            //因为每次删除一个元素之后,下次开始计数都是从删除的那个数后一个开始的
            list<int>::iterator next = ++cur;
            if (next == arr.end())
                next = arr.begin();
            
            --cur;
            arr.erase(cur);
            cur = next;
        }
        return *cur;
    }
};

法二:

用数学的方法,具体思路见书上。
原始编号:0,1…,m-1,m,m+1…,n-1
新的编号:…,0,1,…
可以发现,要得到n个数字的序列中最后剩下的数字,只需要得到n-1个数字的序列中最后剩下的数字,并以此类推。当n=1时,也就是序列中开始只有一个数字0,那么最后剩下的数字也就是0。

迭代的写法

class Solution {
public:
    int LastRemaining_Solution(int n, int m) {
        if(n < 1 || m < 1)
            return -1;
        if(n == 1) return 0;
        int last = 0;
        for(int i=2; i<=n; i++)
            last = (last+m) % i;
        return last;
    }
};

递归的写法

class Solution {
public:
    int LastRemaining_Solution(int n, int m) {
        if(n < 1 || m < 1)
            return -1;
        if(n == 1) return 0;
        return (LastRemaining_Solution(n-1, m)+m) % n;
    }
};

面试题63:股票的最大利润

《剑指Offer(第2版)》读书笔记_第85张图片
思路:(贪心算法)

因为股票就买卖⼀次,就是取数组左边的最小值,然后取数组右边的最大值,那么得到的差值就是最大利润。
时间复杂度O(n)
空间复杂度O(1)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int low = INT_MAX;
        int res = 0;
        int len = prices.size();
        for(int i=0; i<len; i++)
        {
            low = min(low,prices[i]);
            res = max(res,prices[i]-low);
        }
        return res;
    }
};

6.5 发散思维能力

面试题64:求1+2+···+n

《剑指Offer(第2版)》读书笔记_第86张图片
思路:

采用递归的方法,因为题目要求不能使用if、else等关键字,则递归的终止条件不能写成if(n == 0)为递归终止条件了,改写成n && (res += Sum_Solution(n-1));保证两边都为真的情况下递归继续,即保证n>0。

class Solution {
public:
    int Sum_Solution(int n) {
        int res = n;
        n && (res += Sum_Solution(n-1));
        return res;
    }
};

面试题65:不用加减乘除做加法

《剑指Offer(第2版)》读书笔记_第87张图片
思路:

除四则运算之外,也就只剩下位运算。^异或:相同为0,不同为1。
看书上的思路:
正常的加法运算过程:5+17=25
第一步:只做各位相加不进位,此时相加的结果是12(个位数5和7相加不要进位是2,十位数0和1相加结果是1);
第二步:做进位,5+7种有进位,进位的值是10;
第三步:把前面两个结果加起来,12+10的结果是22,刚好5+17=22。
使用位运算的加法运算过程:00101+10001=10110
第一步:各位相加不进位,得到的结果是10100(最后一位两个数都是1,相加的结果是二进制的10,这一步不计进位,因此结果仍然是0);
第二步:记下进位,在这个例子中只在最后一位相加时产生一个进位,结果是二进制的10;
第三步:把前面两步的结果相加,得到的结果是10110,即22。
下面用位运算来替代二进制的加法
0+0、1+1的结果都是0,0+1、1+0的都是1,这点采用异或运算来代替;
考虑进位问题,0+0、1+0、0+1都不会产生进位,只有1+1才会产生进位,这点用与运算来代替,然后再向左移动一位,只有两个数都是1的时候,位与得到的结果是1,其余都是0。
最后把前面两个步骤的结果相加,第三步相加的过程依然是重复前面两步,直到不产生进位为止。

class Solution {
public:
    int Add(int num1, int num2) {
        int sum,carry;
        while(num2)
        {
            sum = num1^num2;
            carry = (num1 & num2) << 1;
            num1 = sum;
            num2 = carry;
        }
        return num1;
    }
};

面试题66:构建乘积数组

《剑指Offer(第2版)》读书笔记_第88张图片

思路:

《剑指Offer(第2版)》读书笔记_第89张图片
下三角:res[i] = res[i-1] * a[i-1];
上三角:从a[i+1]依次乘到最后
时间复杂度:O(n)
空间复杂度:O(1)

class Solution {
public:
    vector<int> constructArr(vector<int>& a) {
        int len = a.size();
        if(len == 0) return {};
        vector<int> res(len,1);

        //先计算左边 a0...ai-1
        res[0] = 1;
        for(int i=1; i<len; i++)
            res[i] = res[i-1] * a[i-1];

        //再计算右边 ai+1...an-1
        int tmp=1;
        for(int i=len-2; i>=0; i--)
        {
            tmp *= a[i+1];
            res[i] *= tmp;
        }
        return res;
    }
};

第7章 两个面试案例

案例一:(面试题67)把字符串转换成整数

在C++中,成员变量的初始化顺序只与它们在类中声明的顺序有关,而与在初始化列表中的顺序无关。
《剑指Offer(第2版)》读书笔记_第90张图片
《剑指Offer(第2版)》读书笔记_第91张图片
思路:

程序思路:
①首先找到字符串中第一个非空字符,若第一个非空字符是’-’,就将标志位设为-1表示负数,若第一个非空字符是’+’,就将标志位设为1表示正数,若第一个字符既不是’-’、’+'或0~9之间,就按题意返回0;(注意若第一个非空字符是符号位,则索引要加1,移动到下一位)
②下面就是正常的字符串转整数的代码res = res*10 + str[i] - ‘0’,但要注意,要对值res进行判断,如果已经超出INT_MAX或INT_MIN,就退出该循环进行后续的判断了,否则程序会报错,因为我们定义的res是long long型,可能输入的字符串str的值会超出long long的范围;
③如果输入的数字后面还有别的字符,按照题目的要求,转换截止于数字,因为它的下一个字符不为数字;
④最后对res进行判断,和标志位进行相乘。

class Solution {
public:
    int strToInt(string str) {
        if(str.size() == 0)
            return 0;
               
        int i = 0;
        int sign = 1; //符号位
        long long res = 0;
        while(str[i] == ' ') //先找到字符串中第一个非空字符
            i++;
        if(str[i] == '-')
        {
            sign = -1;
            i++;
        }          
        else if(str[i] == '+')
        {
            sign = 1;
            i++;
        }           
        else if (str[i] < '0' || str[i] > '9')
            return 0;

        while(i < str.size())
        {
            if(str[i] >= '0' && str[i] <= '9')
            {
                if(res >= INT_MAX || res <= INT_MIN)
                    break;
                res = res*10 + str[i] - '0';
                i++;
            }
            else
                break;
        }
        res *= sign;
        if(res >= INT_MAX)
            return INT_MAX;
        else if(res <= INT_MIN)
            return INT_MIN;
        else
            return res;
    }
};

《剑指Offer(第2版)》读书笔记_第92张图片

案例二:(面试题68)二叉搜索树的最近公共祖先

《剑指Offer(第2版)》读书笔记_第93张图片
思路:

根据题意先对特殊情况进行判断,如果一个节点是自己的祖先,则直接返回p;
二叉搜索树是排序过的,位于左子树的节点都比父节点小,而位于右子树的节点都比父节点大,则需要从根节点开始和两个输入的节点进行比较,
如果当前节点的值大于 p 和 q 的值,说明 p 和 q 应该在当前节点的左子树,因此将当前节点移动到它的左子节点;
如果当前节点的值小于 p 和 q 的值,说明 p 和 q 应该在当前节点的右子树,因此将当前节点移动到它的右子节点;
如果当前节点的值不满足上述两条要求,即当前节点的值位于 p 和 q 节点的值之间时,就是 p 和 q 的最低公共祖先。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(p->left == q || p->right == q) //一个节点也可以是它自己的祖先
            return p;
        if(q->left == p || q->right == p)
            return q;

        while(root != nullptr)
        {
            if(root->val > p->val && root->val > q->val)
                root = root->left;
            else if(root->val < p->val && root->val < q->val)
                root = root->right;
            else
                break;
        }
        return root;
    }
};

案例二:(面试题68)二叉树的最近公共祖先

《剑指Offer(第2版)》读书笔记_第94张图片
解法:https://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/solution/mian-shi-ti-68-ii-er-cha-shu-de-zui-jin-gong-gon-7/

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == NULL) 
            return NULL;
        if(root == p || root == q)
            return root;
        TreeNode* left = lowestCommonAncestor(root->left,p,q);
        TreeNode* right = lowestCommonAncestor(root->right,p,q);
        if(left && right) //如果p和q在异侧
            return root;
        if(left) //如果p和q在左侧
            return left;
        else
            return right;  
    }
};

你可能感兴趣的:(数据结构与算法分析,数据结构,算法)