《剑指offer》题目 C++详细题解

JZ33 二叉搜索树的后序遍历序列

核心考点:BST特征的理解

解题思路:看清楚,本题是二叉搜索树,而二叉搜索树:它或者是一棵空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;后序遍历:先左后右再根,BST的后序序列的合法序列是,对于一个序列S,最后一个元素是x(也就是root节点),如果去掉最后一个元素的序列为T,那么T满足:T可以分成两段,前一段(左子树)小于x,后一段(右子树)大于x,且这两段(子树)都是合法的后序序列,验证思路就是:当前序列,及其子序列必须都满足上述定义。

cpp 复制代码
class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        if(sequence.empty()) return false;
        return dfs(sequence, 0, sequence.size() - 1);
    }

    bool dfs(vector<int> sequence, int start, int end)
    {
        if(start > end)
            return true;
        // 拿到root结点的值
        int root = sequence[end];
        // 先遍历左半部分,也就是整体比root小,拿到左子树序列
        int i = start;
        for(; i < end; i++)
        {
            if(sequence[i] > root)
                break;
        }   
        int mid = i - 1;
        // 再检测右子树是否符合大于root结点的值,从i开始遍历,拿到右子树序列
        for(; i < end; i++)
        {
            if(sequence[i] < root)
                return false;
        }
        // 走到这里,说明当前序列满足情况,但是还需要检测左子树和右子树序列是否也符合
        return dfs(sequence, start, mid) && dfs(sequence, mid + 1, end - 1);
    }
};

JZ84 二叉树中和为某一值的路径(三)

核心考点:简单回溯的使用

解题思路:这是一个典型的DFS回溯的算法,回溯法本质是一个基于DFS的穷举的过程,首先我们需要先添加值,再判断现有结果是否满足条件,不满足就向下递归,当满足条件,就要向上回溯,判断其他结果是否满足。

cpp 复制代码
/**
 * struct TreeNode {
 *	int val;
 *	struct TreeNode *left;
 *	struct TreeNode *right;
 *	TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 * };
 */
class Solution {
    int count = 0; // 全局变量
public:
    int FindPath(TreeNode* root, int sum) {
        if(root == nullptr)
            return 0;
        int val = 0; //判断是否与sum相等
        dfs(root, sum, val);
        FindPath(root->left, sum); // 递归左树
        FindPath(root->right, sum); // 递归右树
        return count;
    }

    void dfs(TreeNode* root, int& sum, int val)
    {
        if(root == nullptr) 
            return;
        // 先添加值     
        val += root->val;
        // 检测条件是否满足
        if(sum == val)
            count++;
        // dfs
        dfs(root->left, sum, val); 
        dfs(root->right, sum, val);
        // 回溯
        val -= root->val;
    }
};

JZ38 字符串的排列

核心考点:全排列问题,DFS

解题思路:全排列问题,可以看做如下多叉树形态

很明显,我们想要得到合适的排列组合,一定是深度优先的,该问题可以把目标串理解成两部分:第一部分:以哪个字符开头,第二部分:剩下的是子问题,所以,我们要让每个字符都要做一遍开头,然后在求解子问题。创建变量用来存储结果,创建一个布尔类型的数组用来标记字符串中的字符是否已经被使用过。定义一个递归函数,它接受两个参数:当前处理的字符串和当前路径。在递归函数内部,首先检查当前路径的长度是否等于原字符串的长度。如果是,则将添加到结果列表中,并返回。如果路径长度还没有达到原字符串长度,则遍历字符串的每一个字符。对于每一个未被使用的字符,将其添加到当前路径中,并标记该字符已被使用。然后递归调用函数,继续生成更长的路径。在递归返回后,回溯:移除路径中的最后一个字符,并取消对该字符的使用标记。由于递归过程中可能会产生重复的排列,所以在生成所有排列后,使用set容器来去除重复项。将结果中的内容插入到set容器中,利用set容器的自动排序和去重特性。然后将set容器中的内容重新赋值给ret结果。

cpp 复制代码
class Solution {
    vector<string> ret; // 返回结果
    bool check[10] = {false}; // 判断当前位置的字母是否被使用
public:
    vector<string> Permutation(string str) {
        string path;
        if(str.size() == 0)
            return ret;
        dfs(str, path);
        // 去重
        set<string> s(ret.begin(), ret.end());
        ret.assign(s.begin(), s.end());
        return ret;
    }

    void dfs(string& str, string path)
    {
        if(path.size() == str.size())
        {
            ret.push_back(path);
            return;
        }

        for(int i = 0; i < str.size(); i++)
        {
            if(check[i] == false)
            {
                path.push_back(str[i]);
                check[i] = true;
                dfs(str, path);
                // 回溯
                path.pop_back();
                check[i] = false;
            }
        }
    }
};

JZ40 最小的K个数

核心考点:topK问题

思路一:直接升序排序,取前n个,这个方法不考虑了

思路二:可以采用最大堆,我们这里使用C++中的p优先级队列进行处理(底层原理类似堆).这里核心思路在于实现topk,我们使用现成的解决方案。

最小堆(小根堆):树中每个非叶子结点都不大于其左右孩子结点的值,也就是根节点最小的堆.

最大堆(大根堆):树中每个非叶子结点大于其左右孩子结点的值,也就是根节点最大的堆.

cpp 复制代码
class Solution {
  public:
    struct cmp {
        bool operator()(const int& k1, const int& k2) {
            return k1 < k2; // 此时需要建立大根堆
        }
    };
    vector<int> GetLeastNumbers_Solution(vector<int>& input, int k) {
        vector<int> ret; // 返回结果
        if(k == 0)
            return ret;
        // 建立一个大小为k的大根堆
        priority_queue<int, vector<int>, cmp> heap;
        for (int i = 0; i < input.size(); i++) 
        {
            if (i < k) 
            {
                //前k个元素,直接放入,priority_queue内部会降序排序
                heap.push(input[i]);
            } 
            else
            {
                if (input[i] < heap.top()) 
                {
                    //如果新的数据,小于queue首部元素(最大值),进行更新   
                    heap.pop();
                    heap.push(input[i]);
                }
            }
        }
        while(!heap.empty())
        {
            ret.push_back(heap.top());
            heap.pop();
        }    
        return ret;
    }
};

JZ85 连续子数组的最大和(二)

核心考点:简单动归问题

方法一:我们可以使用dp完成,定义状态dp(i): 以i下标结尾的最大连续子序列的和,状态递推:dp(i) = max(dp(i-1)+array[i], array[i]) 【这里一定要注意连续关键字】,状态初始化:dp(0) = array[0], maxsum = array[0],此时即可解决。

cpp 复制代码
class Solution {
  public:
    vector<int> FindGreatestSumOfSubArray(vector<int>& array) {
        // 1.dp[i]:以i位置为结尾的最大连续子序列的和(必须包含i下标对应的元素)
        // 2.dp[i] = (dp[i - 1] + array[i], array[i])
        // 3.dp[0] = array[0]
        vector<int> res;
        if (array.size() == 0)
            return res;

        vector<int> dp(array.size(), 0);
        dp[0] = array[0];
        int maxsum = dp[0];
        //滑动区间
        int left = 0, right = 0;
        //记录最长的区间
        int resl = 0, resr = 0;

        for (int i = 1; i < dp.size(); i++) {
            right++;
            //状态转移:连续子数组和最大值
            dp[i] = max(dp[i - 1] + array[i], array[i]);
            //区间新起点
            if (dp[i - 1] + array[i] < array[i])
                left = right;

            //更新最大值
            if (dp[i] > maxsum || dp[i] == maxsum &&
                    (right - left + 1) > (resr - resl + 1)) {
                maxsum = dp[i];
                resl = left;
                resr = right;
            }
        }
        //取数组
        for (int i = resl; i <= resr; i++)
            res.push_back(array[i]);
        return res;
    }
};

如果这个题目只要求我们求解最大连续子数组的和,那么我们还可以更简便来写,这样就能避免使用动态规划造成的空间复杂度的消耗。

cpp 复制代码
//很明显,上面的代码,只会使用dp[i] 和 dp[i-1],所以是有优化的可能的
class Solution {
  public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        //经典且高频dp问题
        //定义状态# f(i): 以i下标结尾的最大连续子序列的和
        int max_value = array[0];
        int total = array[0]; //当前累计的和
        //for 循环,用来检测以i下标结尾的连续子序列的和
        for (int i = 1; i < array.size(); i++) {
            if (total >= 0) {
                //如果之前total累计的和>=0,说明当前数据+total,有利于整体增大
                total += array[i];
            } else {
                //如果之前累计的和<0,说明当前数据+total,不利于整体增大,丢弃之前的所有值
                //这里有一个基本事实,就是之前的连续数据和是确定的。
                //连续,是可以从以前到现在,也可以是从现在到以后。至于要不要加以前,就看以前对整体增大又没有贡献度
                total = array[i];
            }
            //走到这,标示以i下标结尾的最大连续子序列的和已经算出,进行最大值统计
            if (max_value < total) {
                max_value = total;
            }
        }
        return max_value;
    }
};

当然,如果我们对递归和回溯更加敏感,我们会发现这个题目我们还可以使用回溯来解决,这个题目其实就是求子集的一种变形题目,唯一的区别就是这个题目要求子集的数据是连续的,所以此时我们只需要加一个判断即可,直接来上代码,注意:我们这里依然是求解的最大子数组的和,并没有去求解最大子数组的和的区间哦!

cpp 复制代码
class Solution {
    vector<vector<int>> ret;
    vector<int> path;
    unordered_map<int, int> hash;
public:
    int FindGreatestSumOfSubArray(vector<int>& nums) {
        for (int i = 0; i < nums.size(); i++)
        {
            hash[nums[i]] = i;
        }
        dfs(nums, 0);
        
        int maxnum = INT_MIN;
        int sum = 0;
        for (const auto& subVector : ret) { // 遍历每个子向量
            for (size_t i = 0; i < subVector.size(); ++i) { // 遍历子向量中的元素
                sum += subVector[i]; // 累加对应位置的元素
            }
            if (sum > maxnum)
                maxnum = sum;
            sum = 0;
        }
        return maxnum;
    }
    void dfs(vector<int>& nums, int pos)
    {
        ret.push_back(path);
        for (int i = pos; i < nums.size(); i++)
        {
            // 剪掉不连续的分支 - 剪枝
            if(path.empty() || hash[nums[i]] == hash[path.back()] + 1)
            {
                path.push_back(nums[i]);
                dfs(nums, i + 1);
                path.pop_back(); // 恢复现场
            }
        }
    }
};

JZ90 回文数索引

核心思想:字符串的处理

解题思路:可以从两侧进行统计,如果不同,则删除任意一个,在判定是否是回文,如果是,下标就是删除数据的下标,如果不是,就是另一个元素的下标

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;
bool IsPalindrome(string& s, int* start, int* end) {
    int i = 0;
    int j = s.size() - 1;
    bool result = true;
    while (i <= j) {
        if (s[i] != s[j]) {
            result = false;
            break;
        }
        i++, j--;
    }
    if (start != nullptr) *start = i;
    if (end != nullptr) *end = j;
    return result;
}
int main() {
    int num = 0;
    cin >> num;
    while (num) {
        string s;
        cin >> s;
        int start = 0;
        int end = s.size() - 1;
        if (IsPalindrome(s, &start, &end)) {
            cout << -1 << endl; //已经是回文了
        } else {
            s.erase(end, 1);
            if (IsPalindrome(s, nullptr, nullptr)) {
                cout << end << endl;
            } else {
                cout << start << endl;
            }
        }
        num--;
    }
}

JZ45 把数组排成最小的数

核心考点:排序算法的特殊理解

解题思路:这道题很有意思,核心理解是我们对于排序算法的理解,通常我们所理解的排序是比较大小的,如:升序排序的序列意思是:序列中任何一个数字,都比前面的小,比后面的大,我们把说法换一下,对于本题,我们要的有效序列是:序列中任何一个元素y,和它前的任何一个元素x进行有序组合形成xy,比和他后面的任何一个元素z进行有效序列组合yz,满足条件xy < yz(采用字典序列排序)如{32,31},有效组合是3132,所以我们拍完序列之后序列变成{31, 32}.

cpp 复制代码
class Solution {
  public:
    struct cmp 
    {
        bool operator()(const int& k1, const int& k2) {
            string s1 = to_string(k1);
            string s2 = to_string(k2);
            return s1 + s2 < s2 + s1;
        }
    };
    string PrintMinNumber(vector<int>& numbers) {
        // 
        sort(numbers.begin(), numbers.end(), cmp());
        string result;
        for (int i = 0; i < numbers.size(); i++) 
        {
            result += to_string(numbers[i]);
        }
        return result;   
    }
};

其实这个题目还有另外一种做法,使用递归加回溯,将所有的字符进行全排列,然后转为字符串,最后按照字典序排序即可解决。

JZ52 两个链表的第一个公共结点

核心考点:单链表理解,临界条件判定

解题思路:题目要求是单链表,所以如果有交点,则最后一个链表的节点地址一定是相同的,求第一公共节点,本质是让长的链表先走abs(length1-length2)步,后面大家的步调一致,往后找第一个地址相同的节点,就是题目要求的节点,所以需要各自遍历两次链表

cpp 复制代码
/*
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 || !pHead2)
			return nullptr;

		int lenth1 = 0;
		ListNode* cur = pHead1;
		while(cur)
		{
			cur = cur->next;
			lenth1++;
		}

		int lenth2 = 0;
		cur = pHead2;
		while(cur)
		{
			cur = cur->next;
			lenth2++;
		}

		int step = abs(lenth2 - lenth1);
		if(lenth1 > lenth2)
		{
			// 链表pHhead1较长,先走step步
			while(step--)
			{
				pHead1 = pHead1->next;
			}
		}
		else
		{
			// 链表pHhead2较长,先走step步
			while(step--)
			{
				pHead2 = pHead2->next;
			}
		}
		// 同时向后走
		while(pHead1)
		{
			if(pHead1 == pHead2)
				return pHead1;
			pHead1 = pHead1->next;
			pHead2 = pHead2->next;
		}
		return nullptr;

    }
};

JZ55 二叉树的深度

核心考点:二叉树深度的判定方法

思路1. 可以使用递归方式

cpp 复制代码
class Solution {
public:
    int TreeDepth(TreeNode* pRoot) {
		if(pRoot == nullptr)
			return 0;
		return max(TreeDepth(pRoot->left),
			TreeDepth(pRoot->right)) + 1;
    }
};

此时我们会发现题目出现重复子问题,可以将每一层看成是求出左子树和右子树深度更深的哪一个分支数,然后再加上当前层的结点即可求出结果,其实也就相当于下面的代码

cpp 复制代码
/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
	void TreeDepthHelper(TreeNode* pRoot, int depth, int& max)
	{
		if(pRoot == nullptr)
		{
			if(max < depth)
				max = depth;
			return;
		}	
		TreeDepthHelper(pRoot->left, depth + 1, max);
		TreeDepthHelper(pRoot->right, depth + 1, max);
	}

    int TreeDepth(TreeNode* pRoot) {
		if(pRoot == nullptr)
			return 0;
		
		int depth = 0; // 当前层路径的深度
		int max = 0;
		TreeDepthHelper(pRoot, depth, max);
		return max;
    }
};

思路2. 可以层序遍历,统计层数,也就是深度or高度,层序遍历是按照层级顺序遍历树的算法,而队列中存储的是当前层的所有节点,每次循环处理完一层节点后,深度加 1,循环直到队列为空,表示所有节点都已遍历完毕,此时就能统计出二叉树的深度。

cpp 复制代码
/*
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) {
        if (pRoot == nullptr)
            return 0;

        queue<TreeNode*> q;
        q.push(pRoot);

        int res = 0;
        // 层序
        while (!q.empty())
            {
                int sz = q.size();
                res++;
                // 每次把当前层全部处理完
                // 每处理一层,只要还在处理,就说明深度在递增
                while (sz--) // 处理完本层,进入下一层
                {
                    TreeNode* t = q.front();
                    q.pop(); // 去掉当前节点
                    if (t->left) q.push(t->left);
                    if (t->right) q.push(t->right);
                }
            }
	return res;
    }
};

JZ56 数组中只出现一次的两个数字

核心考点:异或理解,位运算

解题思路:问题一:如果只有一个数据单独出现,直接整体异或得到结果,但是这道题是两个不重复的数据,我们可以采取先整体异或,异或结果一定不为0,而其中为1的比特位,不同的两个数据该位置上的数据一定不同,所以我们可以用该比特位进行分组,分组的结果一定是相同数据被分到了同一组,不同数据一定被分到了不同的组,问题就转化成了两个问题一.

注意:相同数据异或的结果是0,任何数和0异或,就是它本身

cpp 复制代码
class Solution {
  public:
    vector<int> FindNumsAppearOnce(vector<int>& nums) {
        vector<int> ret(2, 0);
        // 先将所有数据异或,求出只出现一次的两个数据的异或结果
        // res绝对不可能为0
        int res = 0;
        for (const auto& e : nums)
            res ^= e;

        //从低到高找出第一次出现比特位为1的位置
        int i;
        for (i = 0; i < 32; i++) {
            if (((res >> i) & 1) == 1)
                break;
        }

        // 分为两组
        for (const auto& e : nums) {
            if (((e >> i) & 1) == 1)
                ret[0] ^= e;
            else
                ret[1] ^= e;
        }
        sort(ret.begin(),ret.end());
        return ret;
    }
};
相关推荐
老猿讲编程6 分钟前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye1 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
Chrikk1 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*1 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue1 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man1 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
霁月风2 小时前
设计模式——适配器模式
c++·适配器模式
萧鼎2 小时前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
学地理的小胖砸2 小时前
【一些关于Python的信息和帮助】
开发语言·python
疯一样的码农2 小时前
Python 继承、多态、封装、抽象
开发语言·python