剑指 offer 编程题 C++ 版总结(下)

解题思路:让两个指针分别指向两个链表,当遍历到最末尾的结点后,指针指向另一个链表的头结点。例如:A 和 B 开始的时候分别指向 4 和 5。两个指针分别遍历链表,当 A 遍历到末尾结点的 5 时,下一步让其指向头结点为 5 的结点。而 B,让其指向头结点为 4 的结点。这样,A 和 B 指针在指向公共结点的时候都走了相同的距离,所以它们相遇的时候该结点就是公共结点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode *node1 = headA;
        ListNode *node2 = headB;
        
        while (node1 != node2) {
            if(node1 != NULL)
                node1 = node1->next;
            else
                node1 = headB;
            if(node2 != NULL)
                node2 = node2->next;
            else
                node2 = headA;
        }
        return node1;
    }
};

解题思路:由题可知,数组是排好序的,所以可以先用二分查找找出 target 所在的下标,然后根据下标向前向后查找即可。

class Solution {
public:
    int search(vector<int>& nums, int target) {
            int count = 0, mid, low = 0, high = nums.size()-1;
            
            while(low <= high)
            {
                mid = low + (high - low)/2;
                if(nums[mid] == target){
                    count = searchcount(nums, target, mid);
                    break;
                }
                else if(nums[mid] < target)
                    low = mid + 1;
                else
                    high = mid - 1;
            }
            return count;
    }
    int searchcount(vector<int>& nums, int target, int index)
    {
        int count = 0, left = index - 1, right = index + 1;
        while( left >= 0 ) {
            if(nums[left--] == target)
                count++;
            else 
                break;
        }
        while( right < nums.size() ) {
            if(nums[right++] == target)
                count++;
            else 
                break;
        }
        return count + 1;
    }
};

解题思路:是有序数组,第一个想到的就是二分查找

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int left = 0, right = nums.size();
        while(left < right){
            int mid = left + (right - left)/2;
            if(mid == nums[mid]){
                left = mid+1;
            }else{
                right = mid;
            }
        }
        return left;
    }
};

解题思路:二叉搜索树中序遍历可以得到递增有序序列。将二叉树中序非递归遍历反向操作,每次先右子树入栈即可获得递减有序序列。这样就能很快找到第 k 大的结点。

/**
 * 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:
    int kthLargest(TreeNode* root, int k) {
            stack<TreeNode*> sta;
            TreeNode* p = root;
            int count = 0, result;
            while(!sta.empty() || p != NULL)
            {
                while(p)
                {
                    sta.push(p);
                    p = p->right;
                }
                p = sta.top();
                if(++count == k)
                    result = p->val;
                sta.pop();
                p = p->left;
            }
        return result;
    }
};

解题思路:利用递归计算二叉树的深度。

/**
 * 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:
    int maxDepth(TreeNode* root) {
            if(root == NULL)
                return 0;
            int i = maxDepth(root->left);
            int j = maxDepth(root->right);

            return i > j ? i+1: j+1;
    }
};

解题思路:利用递归计算二叉树的深度。

/**
 * 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:
    bool isBalanced(TreeNode* root) {
        return !root ? true : abs(depth(root->left) - depth(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right);
    }
    int depth(TreeNode* cur) { //计算二叉树的最大深度
        return !cur ? 0 : max(depth(cur->left), depth(cur->right)) + 1;
    }
};

解题思路:(1) 用哈希表。(2) 题目中强调其他数字出现两次,如果只有一个数字的情况下,我们可是使用异或(相同为0,不同为1)得出要找的数字。但是题中要找的数数字有两个,所以这个时候还是需要对整个序列求异或。这样最终得到的是要找的两个数字的异或的结果。此时,我们可以根据得到的值,将原本序列分成两个序列(每个序列除了一个单独数字外,其余的数字都是成对出现的),然后再对这两个序列进行异或操作就能找出两个只出现一次的数字。

class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        unordered_map<int, int> res;
        vector<int> number;
        for (int i = 0; i < nums.size(); i++) {
            res[nums[i]]++;
        }
        for (auto c : nums) {
            if (res[c] == 1) {
                number.emplace_back(c);
            }
        }
        return number;
    }
};


class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        //总共分为两个步骤:(1) 整个序列求异或   (2) 根据异或值划分序列后再求异或
        //第一步:求所有的数异或的结果
        int k = 0;
        for (int num : nums) {
            k ^= num;
        }
        //获得k中最低位的1,k里面两数不同,故肯定有一位1是不同的,可以以此区分两个序列
        int mask = 1;
        while ((k & mask) == 0) {
            mask <<= 1;
        }
        int a = 0; 
        int b = 0;
        //第二步:根据 mask 划分序列后再求异或
        for (int num : nums) {
            if ((num & mask) == 0) {
                a ^= num;  // 找到那位不为1的元素, 其余数为偶数,会自动消去
            } else {
                b ^= num;  // 找到那位为1的元素
            }
        }
        return {a, b};
    }
};

解题思路:(1) 用哈希表。(2) 使用位运算。得注意的是:如果某个数字出现 3 次,那么这个 3 个数字的和肯定能被 3 整除,则其对应二进制位的每一位的和也能被 3 整除。统计数组中每个数字的二进制中每一位的和,判断该和是否能被 3 整除。若可以,则只出现一次的数字的二进制数中那一位为 0,否则为 1。

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        unordered_map<int, int> mp;
        for(int n : nums) mp[n] ++;
        int ans;
        for(auto pr : mp){
            if(pr.second == 1){
                ans = pr.first;
                break;
            }
        }
        return ans;
    }
};


class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ans = 0;
        for(int i = 0; i < 32; ++i){
            int cnt = 0;
            for(int n : nums){
                // n & 1 << i 的值大于0即为真
                if(n & (1 << i)) cnt++;
            }
            // 构造只出现一次的那个数字,采用异或的方法生成二进制中的每一位
            if(cnt % 3 == 1) ans ^= (1 << i);
        }
        return ans;
    }
};

解题思路:题中所给的序列是递增排序序列,所以可以使用双指针 low 和 high 解决该问题。若 low 和 high 的两个元素相加为 target 此时记录相应的值返回即可。若两者的和大于 target,令 high--。反之令 low++。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
            int size = nums.size();
            vector<int> asd(2, 0); 
            int low = 0, high = size-1;
            while(low < high)
            {
                if(nums[low] + nums[high] == target){   
                    asd[0] = nums[low];
                    asd[1] = nums[high];
                }
                if(nums[low] + nums[high] > target){
                    high--;
                }
                else{
                    low++;
                }
            }
            return asd;
    }
};

解题思路:该题所求的是所有和为 target 的连续正整数序列的组合。这里可以使用暴力枚举解决该题。首先需要确定边界,limit 为 target / 2 向下取整。内层循环完成累加操作,若累加和等于 target,则记录相应的数字序列。循环终止的条件是累加和大于 target。

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        vector<vector<int>> vec;
        vector<int> res;
        int sum = 0, limit = (target - 1) / 2; //(target - 1)/2 等效于 target / 2 下取整
        for (int i = 1; i <= limit; ++i) {
            for (int j = i;; ++j) {
                sum += j;                      //完成累加操作
                if (sum > target) {
                    sum = 0;
                    break;
                }
                else if (sum == target) {      //记录数字序列
                    res.clear();
                    for (int k = i; k <= j; ++k) res.emplace_back(k);
                    vec.emplace_back(res);
                    sum = 0;
                    break;
                }
            }
        }
        return vec;
    }
};

下面这个方法是对上述代码的改进。每次操作之后的序列和操作之前的序列相比大部分数字都是一样的,只是增加或减少了一个数字,因此我们可以在前一个序列的和的基础上求操作之后的序列的和。这样可以减少很多不必要的运算,从而提高代码效率。

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        vector<vector<int>> result;
        vector<int> asd;
        if(target < 3)
            return result;
        int low = 1, high = 2;
        int middle = (target+1)/2;
        int sum = low + high;
        while(low < middle)
        {
            if(sum == target)
            {
                Add(result, asd, low, high);
            }

            while(sum > target && low < middle)
            {
                sum -= low;
                low++;
                if(sum == target)
                {
                    Add(result, asd, low, high);
                }   
            }

            high++;
            sum += high;
        }
        return result;
    }
    void Add(vector<vector<int>>& result, vector<int>& asd, int low, int high)
    {
        for(int i = low; i <= high ; i++)
            asd.push_back(i);
        result.push_back(asd);
        asd.clear();
    }
};

若上面的题目不要求组合内的序列是连续的,那该怎么解决?

#include <iostream>
#include <list>
#include <cmath>
using namespace std;

int *a, *b, N, n;
int total = 0;
void find(int sum, int n, int b[])
{
	//递归出口
	if(sum <= 0 || n <= 0)
		return;

	//输出找到的结果
	if(sum == b[n-1])    //表示找到了一个值
	{
		a[n-1] = 1;
		for(int i = 0; i < N; i++)
		{
			if(a[i] == 1)
				cout << b[i] <<" ";
		}
		cout <<endl;
		total++;
	}

	a[n-1] = 1;
	find(sum-b[n-1], n-1, b);    //如果放入n,则从剩余n-1个数中填满sum-n
	a[n-1] = 0;
	find(sum, n-1, b);           //如果不放入n,从n-1个数中填满sum
}

int main()
{
	int sum;
	cout<<"Please Input n&sum:"<<endl;
	cin>>N>>sum;
	a = new int[N];
	b = new int[N];
	for(int i = 0; i < N; i++)
		cin >> b[i];
	n = N;
	find(sum, n, b);
	cout<<"Total:"<<total<<endl;
	//cout << Number <<endl;
    delete []a;
    delete []b;
	return 0;
}

解题思路:(1) 该题是利用空格进行划分,所以可以使用双指针从后往前将字符串进行分割。 (2) 利用 strtok 函数对字符串进行分割,利用 vector 存储分割后的单词,接着完成一个 reverse 操作。最后根据题意,添加相应的空格即可。

class Solution {
public:
    string reverseWords(string s) {
        string str;
        if(s.empty())  //输入字符为空,返回空
            return str;
        cout << s.size() <<endl;
        int i{0}, j{0};  //i,j用来表示单词开始和结束的位置
        j = s.size() - 1;
        for(j; j >= 0; --j)
        {
            if(s[j] != ' ')  //遇到不是空格的
            {
                i = j;
                while(i >=0 && s[i] != ' ')  //从j开始向左寻找单词,i>=0防止越界访问
                    --i;
                for(int k = i + 1; k <= j; ++k)  //单词序列加入字符串中
                    str.push_back(s[k]);
                str.push_back(' ');  //加入一个空格
                j = i;  //改变j的位置
            }
        }
        if(str.size() > 0)
            str.pop_back();  //移除末尾空格
        return str;
    }
};


class Solution {
public:
    string reverseWords(string s) {
            vector<string> asd;
            int size = s.size();
            char* p = new char[size+1];
            strcpy(p, s.c_str());
            char* token = strtok(p, " ");
            while(token != NULL){
                asd.push_back(token);
                token = strtok(NULL, " ");
            }
            reverse(asd.begin(), asd.end());
            string result;
            for(int i = 0; i < asd.size(); i++){
                if(i != asd.size()-1)
                    result += asd[i] + " ";
                else
                    result += asd[i];
            }
            delete []p;
            return result;
    }
};

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        reverse(s.begin(), s.begin() + n);
        reverse(s.begin() + n, s.end());
        reverse(s.begin(), s.end());
        return s;
    }
};

解题思路:直接根据题意做题,用变量记录划窗中的最大值和其下标。当划窗移动的时候,先判断最值下标是否在区间内。如果在,只用对比新添加的元素和最值的大小即可。如果不在,就需要重新寻找划窗中的最大值了。

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int>a;
        if(nums.size() == 0){      
            return a;
        }
        int n = nums.size() - k + 1; // 滑动窗口滚动的次数
        int t = -1;                  // 当前滑窗最大值的下标
        int max;                     // 当前滑窗最大值

        // 遍历,找每个滑窗的最大值
        for(int i = 0;i < n ;i++){
            // 上个滑窗的最大值还在当前滑窗内,如果新加入的元素比最大值还大,更新最大值和其下标     
            if(t >= i){
                if(nums[i+k-1] >= max){
                    t = i+k-1;
                    max = nums[i+k-1];
                }
            }
            // 不在的时候重新遍历当前滑窗,这里效率就低
            else{
                max = nums[i];
                for(int j = i + 1;j < k + i;j++){
                    if(nums[j] >= max){
                        t = j;
                        max = nums[j];
                    }
                }
            }
            // 添加最大值
            a.push_back(max);
        }
        return a;
    }
};

解题思路:解决该题需要用到两个数据结构,queue 和 deque,其中 queue 用于存储数据,deque 用于记录序列的最大值。

push_back():当添加一个新的数据的时候,如果 deque 不为空的情况下且其中的值小于添加的值,那么就将 deque 中的数据 pop_back(),然后将数据同时添加到 deque 和 queue 中。

pop_front():正常 pop 队列元素即可。需要添加的代码是,如果 queue 和 deque 队首元素相等,则两个都要 pop 元素。

max_value():deque 为空时 return -1 ,不为空时 return deque 的队首元素。

class MaxQueue {
    queue<int> q;
    deque<int> d;
public:
    MaxQueue() {
    }
    
    int max_value() {
        if (d.empty())
            return -1;
        return d.front();
    }
    
    void push_back(int value) {
        while (!d.empty() && d.back() < value) {
            d.pop_back();
        }
        d.push_back(value);
        q.push(value);
    }
    
    int pop_front() {
        if (q.empty())
            return -1;
        int ans = q.front();
        if (ans == d.front()) {
            d.pop_front();
        }
        q.pop();
        return ans;
    }
};

解题思路:有一串连续的数字(无重复),这串数字中最大值为 m,最小值为 n,这串数字一共包含 m-n+1 个数字。同样,如果我们能够知道 5 张扑克牌中的最大值 maxValue 和最小值 minValue,那我们就知道,要使它为顺子需要 maxValue - minValue + 1 张牌。

(1) 在查找 maxValue 和 minValue 过程中,跳过大小王。

(2) 如果 maxValue - minValue + 1 > 5,说明题目给的 5 张牌不足以构成顺子,即使里面有大小王,也不够用来填补使它构成顺子,所以返回 false。

(3) 如果 maxValue - minValue + 1 <= 5,说明 5 张牌足以构成顺子(里面的大小王能填补在合适位置),返回 true。

class Solution {
public:
    bool isStraight(vector<int>& nums) {
        bool m[15];
        memset(m, 0, sizeof(m));
        int minValue = 14, maxValue = 0;
        for (int item : nums) {
            if (item == 0) {
                continue;
            }
            if (m[item]) {
                return false;
            }
            m[item] = true;
            minValue = min(minValue, item);
            maxValue = max(maxValue, item);            
        }
        return maxValue - minValue + 1 <= 5;
    }
};

解题思路:这个问题实际上是约瑟夫问题。下面这个例子是N=8 m=3的例子。我们定义F(n,m)表示最后剩下那个人的索引号,因此我们只关系最后剩下来这个人的索引号的变化情况即可。

现在我们知道了G的索引号的变化过程,那么我们反推一下从N = 7 到N = 8 的过程。我们先把被删除的C补充回来,然后右移m个人,发现溢出了,再把溢出的补充在最前面,经过这个操作就恢复了N = 8 的排列了!

因此我们可以推出递推公式 f(8,3) = [ f(7, 3) + 3 ] % 8。进行推广泛化,即 f(n,m) = [ f(n−1,m) + m ]%n 。

法一:递推公式
class Solution {
public:
    int lastRemaining(int n, int m) {
        int pos = 0;             // 最终留下的初始位置
        for(int i = 2; i <= n; i++){
            pos = (pos + m) % i;  // 每次循环右移
        }
        return pos;
    }
};

法二:用链表
int lastRemaining(int n, int m)
{   // n个数字,每次删除第m个数字
		if(n < 1 || m < 1)
			return -1;
			
		int i = 0;
		list<int> a;
		for(i = 0; i < n; i++)
			a.push_back(i);
			
		list<int>::iterator current = a.begin();
		list<int>::iterator next;
		
		while(a.size() > 1){
			for(int i = 1; i < m; i++)
			{
				current++;
				if(current == a.end())
					current = a.begin();
			}
			next = ++current;
			if(next == a.end())
				next = a.begin();
				
			--current;
			a.erase(current);
			current = next; 
		}//end while
		
		return *(current);
}

解题思路:最简单的办法是使用两层循环解决该问题,即外层循环从 0 开始,内层循环使用一个变量记录循环过程中最大的利润。这样算法复杂度就是 O(n^2)。其实在遍历过程中记录当前元素之前的最小值,那么就可以优化掉一次循环。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() == 0)
            return 0;
        int minPrice = prices[0];
        int maxGap = 0;
        for(int i = 0; i < prices.size(); i++){
            minPrice = min(minPrice, prices[i]);        // 更新股票最小值
            maxGap = max(maxGap, prices[i] - minPrice); // 更新利润最大值
        }
        return maxGap;
    }
};

解题思路:根据题意做题即可。

class Solution {
public:
    int strToInt(string str) {
        int i = 0, flag = 1;
        long res = 0; //默认flag = 1,正数
        while (str[i] == ' ') i++;
        if (str[i] == '-') flag = -1;
        if (str[i] == '-' || str[i] == '+') i ++;
        for (; i < str.size() && (str[i] >= '0' && str[i] <= '9'); i++)  {
            res = res * 10 + (str[i] - '0');
            if (res >= INT_MAX && flag == 1) return  INT_MAX;
            if (res > INT_MAX && flag == -1) return  INT_MIN;
        } 
        return flag * res;
    }
};

解题思路:二叉搜索树,其中序遍历有序。所以,当左右子树的值都大于根的值。则移动至根的右子树。当左右子树的值都小于根的值,则移动至根的左子树。若此时,一个结点的值比根大,另一个结点的值比根小,则 这个根就是两者最近祖先结点。

/**
 * 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(p->val > root->val && q->val > root->val) 
            return lowestCommonAncestor(root->right, p, q);
        else if(p->val < root->val && q->val < root->val)
            return lowestCommonAncestor(root->left, p, q);
        else return root;
    }
};

解题思路:递归查询两个节点 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(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)  return root;
        return left ? left : right;    // 只有一个非空则返回该指针,两个都为空则返回空指针
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tyler_Zx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值