剑指 Offer (一):面试需要的基础知识

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

题目一: 找出数组中重复的数字

在这里插入图片描述
测试用例:

  • 长度为 n n n 的数组里包含一个或多个重复的数字
  • 数组中不包含重复的数字
  • 无效输入测试用例 (输入空指针 (假设输入参数为数组);长度为 n n n 的数组中包含 0 ∼ n − 1 0\sim n-1 0n1 之外的数字)

解法一

  • 遍历整个数组,将遍历到的数字存进哈希表。如果发现哈希表内已有该数字,则说明该数字为重复数字。该算法的时间复杂度和空间复杂度均为 O ( n ) O(n) O(n)
  • 能不能进一步将空间复杂度降为 O ( 1 ) O(1) O(1)

解法二

  • 我们注意到数组中的数字都在 0 ∼ n − 1 0\sim n-1 0n1 的范围内。如果这个数组中没有重复的数字,那么当数组排序之后数字 i i i 将出现在下标为 i i i 的位置。因此可以从头到尾依次扫描这个数组中的每个数字,当扫描到下标为 i i i 的数字时,首先比较这个数字 (用 m m m 表示) 是不是等于 i i i,如果是,则接着扫描下一个数字;如果不是,则再拿它和第 m m m 个数字进行比较。如果它和第 m m m 个数字相等,就找到了一个重复的数字;如果它和第 m m m 个数字不相等, 就把第 i i i 个数字和第 m m m 个数字交换, 把 m m m 放到属于它的位置。接下来再重复第 i i i 个位置的比较、交换的过程。如果第 i i i 个位置的数字交换为了 i i i,就继续在第 i + 1 i+1 i+1 个位置进行比较、交换的过程
  • 该算法的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)

在面试中, 如果我们打算修改输入的数据, 则最好先问面试官是不是允许修改

题目二:不修改数组找出重复的数字

在这里插入图片描述
测试用例:

  • 长度为 n n n 的数组里包含一个或多个重复的数字
  • 数组中不包含重复的数字
  • 无效输入测试用例 (输入空指针 (假设输入参数为数组))

解法一

  • 遍历整个数组,将遍历到的数字存进哈希表。如果发现哈希表内已有该数字,则说明该数字为重复数字。该算法的时间复杂度和空间复杂度均为 O ( n ) O(n) O(n)
  • 如果要求空间复杂度为 O ( 1 ) O(1) O(1) 呢?

解法二

  • 考虑为什么数组中会有重复的数字?假如没有重复的数字, 那么在从 1 ∼ n 1\sim n 1n 的范围里只有 n n n 个数字。由于数组里包含超过 n n n 个数字, 所以一定包含了重复的数字。看起来在某范围里数字的个数对解决这个问题很重要
  • 我们使用二分法的思想,把 1 ∼ n 1\sim n 1n 的数字从中间的数字 m m m 分为两个区间。如果前一个区间 1 ∼ m 1\sim m 1m 的数字在数组中的数目超过 m m m, 那么这一半的区间里一定包含重复的数字;否则, 另一半 m + 1 ∼ n m+1\sim n m+1n 的区间里一定包含重复的数字。我们可以继续把包含重复数字的区间一分为二, 直到找到一个重复的数字
    • 以长度为 8 的数组 { 2 , 3 , 5 , 4 , 3 , 2 , 6 , 7 } \{2,3, 5,4,3,2,6,7\} {2,3,5,4,3,2,6,7} 为例分析查找的过程。中间的数字 4 把 1 ∼ 7 1\sim7 17 的范围分为两段,一段是 1 ∼ 4 1\sim4 14, 另一段是 5 ∼ 7 5\sim7 57。接下来我们统计 1 ∼ 4 1\sim4 14 这 4 个数字在数组中出现的次数,它们一共出现了 5 次, 因此这 4 个数字中一定有重复的数字
  • 该算法的时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1),相当于以时间换空间
int getDuplication(const int* numbers, int length)
{
    if(numbers == nullptr || length <= 0)
        return -1;

    int start = 1;
    int end = length - 1;
    while(end >= start)
    {
        int middle = ((end - start) >> 1) + start;
        int count = countRange(numbers, length, start, middle);
        if(end == start)
        {
            if(count > 1)
                return start;
            else
                break;
        }

        if(count > (middle - start + 1))
            end = middle;
        else
            start = middle + 1;
    }
    return -1;
}

int countRange(const int* numbers, int length, int start, int end)
{
    if(numbers == nullptr)
        return 0;

    int count = 0;
    for(int i = 0; i < length; i++)
        if(numbers[i] >= start && numbers[i] <= end)
            ++count;
    return count;
}

  • 从上述分析中我们可以看出, 如果面试官提出不同的功能要求或者性能要求 (时间效率优先、空间效率优先),那么我们最终选取的算法也将不同。这也说明在面试中和面试官交流的重要性,我们一定要在动手写代码之前弄清楚面试官的需求

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

在这里插入图片描述

测试用例:

  • 二维数组中包含查找的数字 (查找的数字是数组中的最大值和最小值;查找的数字介于数组中的最大值和最小值之间)
  • 二维数组中没有查找的数字 (查找的数字大于数组中的最大值;查找的数字小于数组中的最小值;查找的数字在数组的最大值和最小值之间但数组中没有这个数字)
  • 特殊输入测试 (输入空指针)

解法一

  • 由题意可知,在数组中任选一个元素,如果该元素的值比要查找的数字大,那么要查找的数字一定不在右下方;如果该元素的值比要查找的数字小,那么要查找的数字一定不在左上方。因此可以利用分而治之的思想,每次选取二维数组中心的元素进行比较,每次比较都可以排除四分之一的元素
    在这里插入图片描述
bool findNumberInBlock(vector<vector<int>>& matrix, int target, int rstart, int rend, int cstart, int cend)
{
    if (rstart > rend || cstart > cend)
    {
        return false;
    }
    int rmiddle = ((rend - rstart) >> 1) + rstart;
    int cmiddle = ((cend - cstart) >> 1) + cstart;
    bool found = false;
    if (matrix[rmiddle][cmiddle] > target)
    {
        found |= findNumberInBlock(matrix, target, rstart, rend, cstart, cmiddle - 1);
        found |= findNumberInBlock(matrix, target, rstart, rmiddle - 1, cmiddle, cend);
    }
    else if (matrix[rmiddle][cmiddle] < target)
    {
        found |= findNumberInBlock(matrix, target, rstart, rend, cmiddle + 1, cend);
        found |= findNumberInBlock(matrix, target, rstart + 1, rend, cstart, cmiddle);
    }
    else {
        found = true;
    }
    return found;
}

bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
    if (matrix.size() == 0)
    {
        return false;
    }
    return findNumberInBlock(matrix, target, 0, matrix.size() - 1, 0, matrix[0].size() - 1);
}

解法二

  • 更简单的方法是每次选取数组中右上角的数字进行比较 (同理也可以选取数组中左下角的数字)。如果该数字等于要查找的数字, 则查找过程结束;如果该数字大于要查找的数字,则剔除这个数字所在的列;如果该数字小于要查找的数字,则剔除这个数字所在的行。 这样每一步都可以缩小查找的范围,直到找到要查找的数字,或者查找范围为空
bool Find(int* matrix, int rows, int columns, int number)
{
    bool found = false;

    if(matrix != nullptr && rows > 0 && columns > 0)
    {
        int row = 0;
        int column = columns - 1;
        while(row < rows && column >=0)
        {
            if(matrix[row * columns + column] == number)
            {
                found = true;
                break;
            }
            else if(matrix[row * columns + column] > number)
                -- column;
            else
                ++ row;
        }
    }

    return found;
}

面试题 5: 替换空格

在这里插入图片描述

下面假设面试官让我们在原来的字符串上进行替换,并且保证输入的字符串后面有足够多的空余内存

测试用例

  • 输入的字符串中包含空格 (空格位于字符串的最前面;空格位于字符串的最后面; 空格位于字符串的中间;字符串中有连续多个空格)
  • 输入的字符串中没有空格
  • 特殊输入测试 (字符串是一个 nullptr 指针;字符串是一个空字符串;字符串只有一个空格字符;字符串中有连续多个空格)

相关题目

  • 有两个排序的数组 A 1 A_1 A1 A 2 A_2 A2, 内存在 A 1 A_1 A1 的末尾有足够多的空余空间容纳 A 2 A_2 A2。请实现一个函数, 把 A 2 A_2 A2 中的所有数字插入 A 1 A_1 A1 中, 并且所有的数字是排序的

解法一

  • 从后向前替换: 我们可以先遍历一次字符串统计出空格的总数。每替换一个空格,长度增加 2, 因此替换以后字符串的长度等于原来的长度加上 2 乘以空格数目。我们从字符串的后面开始复制和替换。 首先准备两个指针: P 1 P_1 P1 P 2 P_2 P2 P 1 P_1 P1 指向原始字符串的末尾, 而 P 2 P_2 P2 指向替换之后的字符串的未尾
    在这里插入图片描述 接下来我们向前移动指针 P 1 P_1 P1, 逐个把它指向的字符复制到 P 2 P_2 P2 指向的位置,直到碰到第一个空格为止
    在这里插入图片描述碰到第一个空格之后,把 P 1 P_1 P1 向前移动 1 格,在 P 2 P_2 P2 之前插入字符串 “%20”。由于 “%20” 的长度为 3, 同时也要把 P 2 P_2 P2 向前移动 3 格
    在这里插入图片描述按同样的方法处理剩余空格
    在这里插入图片描述 P 1 P_1 P1 P 2 P_2 P2 指向同一位置时,表明所有空格都已经替换完毕
    在这里插入图片描述
void ReplaceBlank(char str[], int length)
{
    if(str == nullptr && length <= 0)
        return;

    /*originalLength 为字符串str的实际长度*/
    int originalLength = 0;
    int numberOfBlank = 0;
    int i = 0;
    while(str[i] != '\0')
    {
        ++ originalLength;

        if(str[i] == ' ')
            ++ numberOfBlank;

        ++ i;
    }

    /*newLength 为把空格替换成'%20'之后的长度*/
    int newLength = originalLength + numberOfBlank * 2;
    if(newLength > length)
        return;

    int indexOfOriginal = originalLength;
    int indexOfNew = newLength;
    while(indexOfOriginal >= 0 && indexOfNew > indexOfOriginal)
    {
        if(str[indexOfOriginal] == ' ')
        {
            str[indexOfNew --] = '0';
            str[indexOfNew --] = '2';
            str[indexOfNew --] = '%';
        }
        else
        {
            str[indexOfNew --] = str[indexOfOriginal];
        }

        -- indexOfOriginal;
    }
}

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

在这里插入图片描述

测试用例:

  • 功能测试 (输入的链表有多个节点;输入的链表只有一个节点)
  • 特殊输入测试 (输入的链表头节点指针为 nullptr)

  • 可以很容易想到用去处理,不过栈中存链表节点的指针应该比直接存数据更好:
void PrintListReversingly_Iteratively(ListNode* pHead)
{
    std::stack<ListNode*> nodes;

    ListNode* pNode = pHead;
    while(pNode != nullptr)
    {
        nodes.push(pNode);
        pNode = pNode->m_pNext;
    }

    while(!nodes.empty())
    {
        pNode = nodes.top();
        printf("%d\t", pNode->m_nValue);
        nodes.pop();
    }
}
  • 相应地也可以用递归去实现,但当链表非常长的时候, 就会导致函数调用的层级很深, 从而有可能导致函数调用栈溢出。显然用栈基于循环实现的代码的鲁棒性要好一些
void PrintListReversingly_Recursively(ListNode* pHead)
{
    if(pHead != nullptr)
    {
        if (pHead->m_pNext != nullptr)
        {
            PrintListReversingly_Recursively(pHead->m_pNext);
        }
 
        printf("%d\t", pHead->m_nValue);
    }
}

面试题 7: 重建二叉树

在这里插入图片描述在这里插入图片描述
测试用例:

  • 普通二叉树 (完全二叉树;不完全二叉树)
  • 特殊二叉树 (所有节点都没有右子节点的二叉树;所有节点都没有左子节点的二叉树;只有一个节点的二叉树)
  • 特殊输入测试 (二叉树的根节点指针为 nullptr; 输入的前序遍历序列和中序遍历序列不匹配)

  • 常规题,可用递归解决
#include "..\Utilities\BinaryTree.h"
#include <exception>
#include <cstdio>

BinaryTreeNode* ConstructCore(int* startPreorder, int* endPreorder, int* startInorder, int* endInorder);

BinaryTreeNode* Construct(int* preorder, int* inorder, int length)
{
    if(preorder == nullptr || inorder == nullptr || length <= 0)
        return nullptr;

    return ConstructCore(preorder, preorder + length - 1,
        inorder, inorder + length - 1);
}

BinaryTreeNode* ConstructCore
(
    int* startPreorder, int* endPreorder, 
    int* startInorder, int* endInorder
)
{
    // 前序遍历序列的第一个数字是根结点的值
    int rootValue = startPreorder[0];
    BinaryTreeNode* root = new BinaryTreeNode();
    root->m_nValue = rootValue;
    root->m_pLeft = root->m_pRight = nullptr;

    if(startPreorder == endPreorder)
    {
        if(startInorder == endInorder && *startPreorder == *startInorder)
            return root;
        else
            throw std::exception("Invalid input.");
    }

    // 在中序遍历中找到根结点的值
    // 这里每次都要遍历中序序列去寻找根结点的位置,为了进一步节省时间,我们可以事先
    // 用 unordered_map 去存储中序序列中每个值与其位置的映射关系
    int* rootInorder = startInorder;
    while(rootInorder <= endInorder && *rootInorder != rootValue)
        ++ rootInorder;

    if(rootInorder == endInorder && *rootInorder != rootValue)
        throw std::exception("Invalid input.");

    int leftLength = rootInorder - startInorder;
    int* leftPreorderEnd = startPreorder + leftLength;
    if(leftLength > 0)
    {
        // 构建左子树
        root->m_pLeft = ConstructCore(startPreorder + 1, leftPreorderEnd, 
            startInorder, rootInorder - 1);
    }
    if(leftLength < endPreorder - startPreorder)
    {
        // 构建右子树
        root->m_pRight = ConstructCore(leftPreorderEnd + 1, endPreorder,
            rootInorder + 1, endInorder);
    }

    return root;
}

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

在这里插入图片描述

测试用例:

  • 普通二叉树 (完全二叉树;不完全二叉树)
  • 特殊二叉树 (所有节点都没有右子节点的二叉树;所有节点都没有左子节点的二叉树;只有一个节点的二叉树;二叉树的根节点指针为 nullptr)
  • 不同位置的节点的下一个节点 (下一个节点为当前节点的右子节点、右子树的最左子节点、父节点、跨层的父节点等当前节点没有下一个节点)

  • 主要就是需要分类讨论,(1) 如果一个节点有右子树, 那么它的下一个节点就是它的右子树中的最左子节点;(2) 如果一个节点没有右子树且是它父节点的左子节点, 那么它的下一个节点就是它的父节点;(3) 如果一个节点既没有右子树,并且它还是它父节点的右子节点, 那么我们可以沿着指向父节点的指针一直向上遍历, 直到找到一个是它父节点的左子节点的节点,如果没有找到,则说明该节点为中序遍历的最后一个节点
BinaryTreeNode* GetNext(BinaryTreeNode* pNode)
{
    if(pNode == nullptr)
        return nullptr;

    BinaryTreeNode* pNext = nullptr;
    if(pNode->m_pRight != nullptr)
    {
        BinaryTreeNode* pRight = pNode->m_pRight;
        while(pRight->m_pLeft != nullptr)
            pRight = pRight->m_pLeft;

        pNext = pRight;
    }
    else if(pNode->m_pParent != nullptr)	// 这里要记得判断是否有父节点
    {
        BinaryTreeNode* pCurrent = pNode;
        BinaryTreeNode* pParent = pNode->m_pParent;
        while(pParent != nullptr && pCurrent == pParent->m_pRight)
        {
            pCurrent = pParent;
            pParent = pParent->m_pParent;
        }

        pNext = pParent;
    }

    return pNext;
}

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

在这里插入图片描述

测试用例:

  • 往空的队列里添加、删除元素
  • 往非空的队列里添加、删除元素
  • 连续删除元素直至队列为空

相关题目

  • 用两个队列实现一个栈
    在这里插入图片描述

  • 思路:元素入队时永远入栈 stack1;元素出队时,若 stack2 为空,则把 stack1 中的元素逐个弹出并压入 stack2,若 stack2 不为空,则说明在 stack2 中的栈顶元素是最先进入队列的元素,可以直接弹出
template<typename T> void CQueue<T>::appendTail(const T& element) 
{
	stack1.push(element);
}
    
template<typename T> T CQueue<T>::deleteHead() 
{
    int res = -1;	// return -1 if queue is empty
    if (stack2.empty())
    {
        while (!stack1.empty())
        {
            stack2.push(stack1.top());
            stack1.pop();
        }
    }
    if (!stack2.empty())
    {
        res = stack2.top();
        stack2.pop();
    }
    return res;
}

面试题 10: 斐波那契数列

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

测试用例:

  • 功能测试 (如输入 3、5、10 等)
  • 边界值测试 (如输入 0、1、2)
  • 性能测试 (输入较大的数字,如 40、50、100 等)

本题扩展:

  • 在青蛙跳台阶的问题中,如果把条件改成: 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级… …它也可以跳上 n n n 级,此时该青蛙跳上一个 n n n 级的台阶总共有多少种跳法?我们用数学归纳法可以证明 f ( n ) = 2 n − 1 f(n)=2^{n-1} f(n)=2n1

相关题目:

  • 我们可以用 2 × 1 2\times1 2×1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 8 个 2 × 1 2\times1 2×1 的小矩形无重叠地覆盖一个 2 × 8 2\times8 2×8 的大矩形,总共有多少种方法?
    在这里插入图片描述

解法一: 递归

long long Fibonacci_Solution1(unsigned int n)
{
    if(n <= 0)
        return 0;

    if(n == 1)
        return 1;

    return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2);
}
  • 递归算法虽然简洁但效率极低,需要进行大量的重复计算,时间复杂度是指数级的:
    在这里插入图片描述

解法二: 动态规划

long long Fibonacci_Solution2(unsigned n)
{
    int result[2] = {0, 1};
    if(n < 2)
        return result[n];

    long long  fibNMinusOne = 1;
    long long  fibNMinusTwo = 0;
    long long  fibN = 0;
    for(unsigned int i = 2; i <= n; ++ i)
    {
        fibN = fibNMinusOne + fibNMinusTwo;

        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }

     return fibN;
}
  • 动态规划通过自底向上的思想避免了大量重复计算,将时间复杂度优化到了 O ( n ) O(n) O(n)

解法三: 矩阵快速幂

  • 使用数学归纳法可以证得
    在这里插入图片描述因此我们只需计算
    在这里插入图片描述上述矩阵为对称矩阵,可以对其进行谱分解,只需 O ( 1 ) O(1) O(1) 就可计算出结果;也可以考虑如下乘方的性质
    在这里插入图片描述从上面的公式中我们可以看出,我们想求得 n n n 次方,就要先求得 n / 2 n/2 n/2 次方,再把 n / 2 n/2 n/2 次方的结果平方一下即可。这可以用递归的思路实现,只需 O ( log ⁡ n ) O(\log n) O(logn) 的复杂度 (但由于隐含的时间常数较大,很少会有软件采用这种算法)

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

在这里插入图片描述

测试用例

  • 功能测试 (输入的数组是升序排序数组的一个旋转,数组中有重复数字或者没有重复数字)
  • 边界值测试 (输入的数组是一个升序排序的数组只包含一个数字的数组)
  • 特殊输入测试 (输入 nullptr 指针)

  • 本题思路为二分查找难点是要考虑一些特殊情况,思考不周全的话就很容易出错。我们用两个指针分别指向数组的第一个元素和最后一个元素。按照题目中旋转的规则, 第一个元素应该是大于或者等于最后一个元素的。接着我们可以找到数组中间的元素, 如果该中间元素大于等于第一个指针指向的元素,则说明该中间元素位于前面的递增子数组,此时数组中最小的元素应该位于该中间元素的后面,因此可以把第一个指针指向该中间元素, 这样可以缩小寻找的范围,移动之后的第一个指针仍然位于前面的递增子数组。同样, 如果该中间元素小于等于第二个指针指向的元素,则说明该中间元素位于后面的递增子数组,此时数组中最小的元素应该位于该中间元素的前面,因此可以把第二个指针指向该中间元素,这样可以缩小寻找的范围,移动之后的第二个指针仍然位于后面的递增子数组。按照上述思路, 第一个指针总是指向前面递增数组的元素, 而第二个指针总是指向后面递增数组的元素。最终第一个指针将指向前面子数组的最后一个元素, 而第二个指针会指向后面子数组的第一个元素。也就是它们最终会指向两个相邻的元素, 而第二个指针指向的刚好是最小的元素。这就是循环结束的条件
  • 但注意,上面的讨论其实少考虑了两个特例
    • (1) 如果把排序数组的前面的 0 个元素搬到最后面,即排序数组本身,这仍然是数组的一个旋转,此时 数组中的第一个数字就是最小的数字,可以直接返回
    • (2) 如果第一个指针、第二个指针和中间指针所指数均相同,则我们无法判断最小数字是在中间位置的左侧还是右侧,此时不得不进行顺序查找
      在这里插入图片描述
#include <cstdio>
#include <exception>

int MinInOrder(int* numbers, int index1, int index2);

int Min(int* numbers, int length)
{
    if(numbers == nullptr || length <= 0)
        throw new std::exception("Invalid parameters");
 
    int index1 = 0;
    int index2 = length - 1;
    int indexMid = index1;
    while(numbers[index1] >= numbers[index2])
    {
        // 如果index1和index2指向相邻的两个数,
        // 则index1指向第一个递增子数组的最后一个数字,
        // index2指向第二个子数组的第一个数字,也就是数组中的最小数字
        if(index2 - index1 == 1)
        {
            indexMid = index2;
            break;
        }
 
        // 如果下标为index1、index2和indexMid指向的三个数字相等,
        // 则只能顺序查找
        indexMid = (index1 + index2) / 2;
        if(numbers[index1] == numbers[index2] && numbers[indexMid] == numbers[index1])
            return MinInOrder(numbers, index1, index2);

        // 缩小查找范围
        if(numbers[indexMid] >= numbers[index1])
            index1 = indexMid;
        else if(numbers[indexMid] <= numbers[index2])
            index2 = indexMid;
    }
 
    return numbers[indexMid];
}

int MinInOrder(int* numbers, int index1, int index2)
{
    int result = numbers[index1];
    for(int i = index1 + 1; i <= index2; ++i)
    {
        if(result > numbers[i])
            result = numbers[i];
    }

    return result;
}

面试题 12: 矩阵中的路径

在这里插入图片描述

测试用例

  • 功能测试 (在多行多列的矩阵中存在或者不存在路径)
  • 边界值测试 (矩阵只有一行或者只有一列:矩阵和路径中的所有字母都是相同的)
  • 特殊输入测试 (输入 nullptr 指针)

  • 典型的使用回溯法解决的题目
#include <cstdio>
#include <string>
#include <stack>

using namespace std;

bool hasPathCore(const char* matrix, int rows, int cols, int row, int col, const char* str, int& pathLength, bool* visited);

bool hasPath(const char* matrix, int rows, int cols, const char* str)
{
    if(matrix == nullptr || rows < 1 || cols < 1 || str == nullptr)
        return false;

    bool *visited = new bool[rows * cols];
    memset(visited, 0, rows * cols);

    int pathLength = 0;
    for(int row = 0; row < rows; ++row)
    {
        for(int col = 0; col < cols; ++col)
        {
            if(hasPathCore(matrix, rows, cols, row, col, str,
                pathLength, visited))
            {
                return true;
            }
        }
    }

    delete[] visited;

    return false;
}

bool hasPathCore(const char* matrix, int rows, int cols, int row,
    int col, const char* str, int& pathLength, bool* visited)
{
    if(str[pathLength] == '\0')
        return true;

    bool hasPath = false;
    if(row >= 0 && row < rows && col >= 0 && col < cols
        && matrix[row * cols + col] == str[pathLength]
        && !visited[row * cols + col])
    {
        ++pathLength;
        visited[row * cols + col] = true;

        hasPath = hasPathCore(matrix, rows, cols, row, col - 1,
            str, pathLength, visited)
            || hasPathCore(matrix, rows, cols, row - 1, col,
                str, pathLength, visited)
            || hasPathCore(matrix, rows, cols, row, col + 1,
                str, pathLength, visited)
            || hasPathCore(matrix, rows, cols, row + 1, col,
                str, pathLength, visited);

        if(!hasPath)
        {
            --pathLength;
            visited[row * cols + col] = false;
        }
    }

    return hasPath;
}

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

在这里插入图片描述
测试用例

  • 功能测试 (方格为多行多列; k k k 为正数)
  • 边界值测试 (方格只有一行或者只有一列; k k k 等于 0)
  • 特殊输入测试 ( k k k 为负数)

  • 典型的回溯法解决的题目。要注意的是题意并不是求机器人能走的最长路径,而是求机器人能走的最大面积
#include <cstdio>

int movingCountCore(int threshold, int rows, int cols, int row, int col, bool* visited);
bool check(int threshold, int rows, int cols, int row, int col, bool* visited);
int getDigitSum(int number);

int movingCount(int threshold, int rows, int cols)
{
    if(threshold < 0 || rows <= 0 || cols <= 0)
        return 0;

    bool *visited = new bool[rows * cols];
    for(int i = 0; i < rows * cols; ++i)
        visited[i] = false;

    int count = movingCountCore(threshold, rows, cols,
        0, 0, visited);

    delete[] visited;

    return count;
}

int movingCountCore(int threshold, int rows, int cols, int row,
    int col, bool* visited)
{
    int count = 0;
    if(check(threshold, rows, cols, row, col, visited))
    {
        visited[row * cols + col] = true;

        count = 1 + movingCountCore(threshold, rows, cols,
            row - 1, col, visited)
            + movingCountCore(threshold, rows, cols,
                row, col - 1, visited)
            + movingCountCore(threshold, rows, cols,
                row + 1, col, visited)
            + movingCountCore(threshold, rows, cols,
                row, col + 1, visited);
    }

    return count;
}

bool check(int threshold, int rows, int cols, int row, int col,
    bool* visited)
{
    if(row >= 0 && row < rows && col >= 0 && col < cols
        && getDigitSum(row) + getDigitSum(col) <= threshold
        && !visited[row* cols + col])
        return true;

    return false;
}

int getDigitSum(int number)
{
    int sum = 0;
    while(number > 0)
    {
        sum += number % 10;
        number /= 10;
    }

    return sum;
}

面试题 14: 剪绳子

在这里插入图片描述

注: 绳子的长度也必须为整数

测试用例:

  • 功能测试 (绳子的初始长度大于 5)
  • 边界值测试 (绳子的初始长度分别为 0、1、2、3、4)

解法一: 动态规划 O ( n 2 ) O(n^2) O(n2)

  • 首先定义函数 f ( n ) f(n) f(n) 为把长度为 n n n 的绳子剪成若干段后各段长度乘积的最大值,递推公式为
    f ( n ) = max ⁡ { max ⁡ { f ( i ) , i } × max ⁡ { f ( n − i ) , n − i } } f(n)=\max\bigg\{\max\{f(i),i\}\times \max\{f(n-i),n-i\}\bigg\} f(n)=max{max{f(i),i}×max{f(ni),ni}}注意题意中划分时段数必须大于 1,也就是必须进行划分,因此在递推时需要考虑子段不进行划分的情况
int maxProductAfterCutting_solution1(int length)
{
  	vector<int> ans;
    ans.resize(length + 1);
    ans[1] = 1;

    for (int i = 2; i <= length; ++i)
    {
        for (int k = 1; k <= i / 2; ++k)
        {
            int max_k = max(ans[k], k) * max(ans[i - k], i - k);
            if (max_k > ans[i])
            {
                ans[i] = max_k;
            }
        }
    }
    return ans[length];
}

解法二: 贪心算法 O ( 1 ) O(1) O(1)

  • 如果我们按照如下的策略来剪绳子,则得到的各段绳子的长度的乘积将最大: n ≥ 5 n\geq5 n5 时,尽可能多地剪长度为 3 的绳子;当剩下的绳子长度为 4 时,把绳子剪成两段长度为 2 的绳子;具体证明见 leetcode (神仙解法,反正我肯定想不出来)
int maxProductAfterCutting_solution2(int length)
{
    if(length < 2)
        return 0;
    if(length == 2)
        return 1;
    if(length == 3)
        return 2;

    // 尽可能多地减去长度为 3 的绳子段
    int timesOf3 = length / 3;

    // 当绳子最后剩下的长度为 4 的时候,不能再剪去长度为 3 的绳子段。
    // 此时更好的方法是把绳子剪成长度为 2 的两段,因为 2*2 > 3*1。
    if(length - timesOf3 * 3 == 1)
        timesOf3 -= 1;

    int timesOf2 = (length - timesOf3 * 3) / 2;

    return (int) (pow(3, timesOf3)) * (int) (pow(2, timesOf2));
}

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

在这里插入图片描述

测试用例

  • 正数 (包括边界值 1、0x7FFFFFFF)
  • 负数 (包括边界值 0x80000000、0xFFFFFFFF)
  • 0

相关题目

  • (1) 判断一个整数是不是 2 的整数次方
  • (2) 输入两个整数 m m m n n n, 计算需要改变 m m m 的二进制表示中的多少位才能得到 n n n。比如 10 的二进制表示为 1010, 13 的二进制表示为 1101, 需要改变 1010 中的 3 位才能得到 1101

可能引起死循环的解法

  • 最基本的思路就是不断用整数最低位与 1 做位与运算,再把整数右移一位 (注意,右移不能换成除法,除法的效率比移位运算低得多)
int NumberOf1(int n)
{
    int count = 0;
    while (n)
    {
        count += n & 1;
        n >>= 1;
    }

    return count;
}
  • 如果 n n n 为负数,右移时就会在最高位不断补 1,使得上述算法陷入死循环

常规解法

  • 为了避免死循环, 我们可以不右移输入的数字 n n n。首先把 n n n 和 1 做与运算,判断 n n n 的最低位是不是为 1。接着把 1 左移一位再和 n n n 做与运算,就能判断 n n n 的次低位是不是 1… 这样反复左移, 每次都能判断 n n n 的其中一位是不是 1
int NumberOf1_Solution1(int n)
{
    int count = 0;
    unsigned int flag = 1;
    while (flag)
    {
        count += n & flag;
        flag <<= flag;
    }

    return count;
}

更好的解法:整数中有几个 1 就只需要循环几次

  • 在分析这种算法之前,我们先来分析把一个数减去 1 的情况。如果把一个整数减去 1, 都是把最右边的 1 变成 0。如果它的右边还有 0, 则所有的 0 都变成 1, 而它左边的所有位都保持不变。接下来我们把一个整数和它减去 1 的结果做位与运算,相当于把它最右边的 1 变成 0
  • 总结一下可得如下算法:把一个整数减去 1, 再和原整数做与运算,会把该整数最右边的 1 变成 0。那么一个整数的二进制表示中有多少个 1, 就可以进行多少次这样的操作 (这是一个比较有用的结论,可以记一下)
int NumberOf1_Solution2(int n)
{
    int count = 0;

    while (n)
    {
        ++count;
        n = (n - 1) & n;
    }

    return count;
}

参考文献

  • 《剑指 Offer》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值