剑指offer(四)

31. 整数中1出现的次数

整数中1出现的次数(从1到n整数中1出现的次数)_牛客题霸_牛客网 (nowcoder.com)

法一

题目怎么说,我就怎么做。主打的就是一个暴力。但数据量一上来就超时了。

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

法二

分情况讨论求解吧。1出现的位数为1在n的各个数位上出现的次数之和。

剑指offer-整数中1出现的次数(从1到n整数中1出现的次数)-CSDN博客

举例如下,

对于n = 1140143

  • 假设base为10,将其分组为high = n / base / 10 = 11401cur = n / base % 10 = 4low = n % base = 3。此时,cur > 1,故1在“十位”上出现的次数有(0~11401)x(0~9)(high + 1)*base
  • 假设base为100,将其分组为high = n / base / 10 = 1140cur = n / base % 10 = 1low = n % base = 43。此时,cur == 1,故1出现的次数需要分情况讨论
    • 当high为(0~1139)时,low依旧可以在(0~99)中间任意选择。故1出现的次数为high * base
    • 当high为1140时,low必须在(0~low)中间选择。否则结果会大于n。故1出现的次数为low + 1
    • 综合两种情况,此时1在“百位”上出现的次数为high * base + (low + 1)
  • 假设base为1000,将其分组为high = n / base / 10 = 114cur = n / base % 10 = 0low = n % base = 143。此时,high只能在(0~113)中选择,base可以在(0~999)中选择。故1在“千位”上出现的次数为high * base

最后只要将base从1到n走一遍,就将所有情况统计全了。

class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n) {
        int base = 1;
        int ans = 0;
        while(base <= n){
            int low = n % base;
            int high = n / base;
            int cur = high % 10;
            high /= 10;
            if(cur > 1){
                ans += (high + 1) * base;
            }else if(cur == 1){
                ans += high * base + (low + 1);
            }else{  
                ans += high * base;
            }
            base *= 10;
        }
        return ans;
    }
};

法三

与法二相似,还是逐位统计1的个数,但是从最高位开始分段处理。

例如对于1145。最高位为high = 1,最高位权重为base = 1000

  • 最高位是1
    • 最高位1的次数为last + 1(1000-1145),这里对于(1000-1145)只计算其千位上的1,(000~145)这三位上的留在之后计算。
    • 去除最高位的次数,0-999的1的个数为high*NumberOf1Between1AndN_Solution(base - 1)
    • 剩余部分1的个数为NumberOf1Between1AndN_Solution(last)(这部分为145中1的个数,但与0-999中的不冲突,因为此时的代表最高位为1时,剩余部分1的个数)
  • 最高位不是1,例如是2
    • 最高位1的次数为base(1000-1999)
    • 去除最高位的次数,即0-999,1000-1999high*NumberOf1Between1AndN_Solution(pow - 1)
    • 剩余部分1的个数为NumberOf1Between1AndN_Solution(last)

以上两种情况的差别仅在于最高位1的次数。故可以合并处理。

class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n) {
        if(n <= 0) return 0;
        if(n < 10) return 1;
        int high = n,base = 1;
        while(high >= 10){
            high /= 10;
            base *= 10;
        }
        int last = n - high * base;
        int cnt = high == 1 ? last + 1 : base;
        return cnt + high*NumberOf1Between1AndN_Solution(base - 1) + NumberOf1Between1AndN_Solution(last);
    }
};

32. 把数组排成最小的数

把数组排成最小的数_牛客题霸_牛客网 (nowcoder.com)

该问题所求其实就是数组中各个数字如何排列拼接而成的字符串表示的值最小。对于a和b两个数,如果ab大于ba那么a一定在b前面。

例如,a=12,b = 34。则ab=1234,ba=3412,最终排列时b一定在a前面。

故,可以数自定义一种比较方式利用sort实现排序。

class Solution {
public:
    string PrintMinNumber(vector<int>& numbers) {
        string ans = "";
        if(numbers.size() == 0) return ans;
        vector<string> nums;
        for(auto& num : numbers){
            nums.push_back(to_string(num));
        }
        sort(nums.begin(),nums.end(),[](string& a,string& b){
            return a + b < b + a;
        });
        for(auto& num : nums){
            ans += num;
        }
        return ans;
    }
};

注:若sort函数的第三个参数不使用lambda表达式而使用比较函数,该函数需要定义为静态函数。

这是因为在类内定义的非static成员函数在经过编译后会隐式添加一个this指针参数,而标准库的sort()函数的第三个cmp函数指针参数中并没有这样this指针参数,因此会出现输入的cmp参数和sort()要求的参数不匹配,从而导致了:error: reference to non-static member function must be called

正确写法如下,

class Solution {
public:
    static bool cmp(string& a,string & b){
        return a + b < b + a;
    }
    string PrintMinNumber(vector<int>& numbers) {
        string ans = "";
        if(numbers.size() == 0) return ans;
        vector<string> nums;
        for(auto& num : numbers){
            nums.push_back(to_string(num));
        }
        sort(nums.begin(),nums.end(),cmp);
        for(auto& num : nums){
            ans += num;
        }
        return ans;
    }
};

33. 第N个丑数

丑数_牛客题霸_牛客网 (nowcoder.com)

法一

三指针法。

丑数就是只包含质因子2、3、5的数。

  • 从1开始逐个判断是否为丑数是一个直观的解决方案。具体而言,可以依次检查能否被2、3、5整除。如果能被2整除,则除以2;如果能被3整除,则除以3;最后,如果剩下的是1,那么就是丑数,否则不是。
  • 上面的重复计算很多。比如,假设我们知道4是丑数,8首先除以2得到4之后完全没必要继续除下去了,可以得到8也是丑数的结果。故,可以用空间换时间。
    • 只计算丑数,根据定义。丑数应该是一个丑数乘以2、3、5得到的结果(1除外)。但在乘的过程中如何保证得到的一系列丑数是有序的方便我们找到第N个呢?
    • 用三个指针分别指向当前乘以2、3、5的丑数,初始值都为0,代表第0个丑数1。例如,indexThree = 2表示,第二个丑数乘以3。
    • 每次循环中,从三个指针的计算结果中选出最小的一个作为新的丑数,并将对应的指针加1,表示下次用更大的丑数乘以相应的因子。例如,第一次循环中,最小的丑数为2,所以将indexTwo加1,表示下次用第二个丑数2乘以2。
    • 这样便可以得到一个有序的丑数序列。
class Solution {
public:
    int GetUglyNumber_Solution(int index) {
        if(index < 7) return index;
        vector<int> ans(index);
        ans[0] = 1;
        int indexTwo = 0,indexThree = 0,indexFive = 0;
        for(int i = 1;i<index;++i){
            int minNum = min(min(ans[indexTwo] * 2,ans[indexThree] * 3),ans[indexFive]* 5);
            if(minNum == ans[indexTwo] * 2) indexTwo++;
            if(minNum == ans[indexThree] * 3) indexThree++;
            if(minNum == ans[indexFive] * 5) indexFive++;
            ans[i] = minNum;
        }
        return ans[index - 1];
    }
};

法二

小顶堆 + 哈希表

  1. 用小顶堆记录丑数,用哈希表去重,数组记录2、3、5乘数因子。
  2. 1作为第一个丑数先入堆,后面的丑数都是其不断乘以2、3、5的结果。
  3. 每次从小顶堆中弹出最小的元素,一共弹出index次。
  4. 对于每个弹出的元素,用其构造后面的丑数,即分别乘以2、3、5,若是不重复,则加入堆中。
class Solution {
public:
    int GetUglyNumber_Solution(int index) {
        if(index == 0) return 0;
        vector<int> fac = {2,3,5};
        priority_queue<long long,vector<long long>,greater<long long>> pq;
        unordered_set<long long> st;
        // 放入初始丑数1
        st.insert(1);
        pq.push(1);
        long long ans = 0;
        for(int i = 0;i<index;i++){
            ans = pq.top();
            pq.pop();
            for(int j = 0;j<3;j++){
                long long next = ans * fac[j];
                if(st.find(next) == st.end()){
                    st.insert(next);
                    pq.push(next);
                }
            }
        }
        return (int)ans;
    }
};

注:

  • 在计算过程中会有数超过int范围,故哈希表和小根堆都要用long long。那么为什么法一不需要呢?
    • 因为法一乘数因子是各自独立乘以属于自己的当前丑数。
    • 而法二是找一个最小的丑数然后分别乘以2、3、5。
    • 这时候就可能遇见这种情况:需要求第7个丑数,但是在第4个丑数乘以5的时候已经求到了第9个丑数,虽然第7个丑数还是int的范围内,但第9个丑数已经超过int的范围了。

34. 第一个只出现一次的字符

第一个只出现一次的字符_牛客题霸_牛客网 (nowcoder.com)

法一

题目中说字符串只由字母组成,故不需要用哈希表来存储,直接用一个数组来充当一个简易的哈希表。所有的字母中ASCII码最小的是A(65),最大的是z(122),故需要一个大小为122-65+1=58的数组来记录这些字符出现的次数。

class Solution {
public:
    int FirstNotRepeatingChar(string str) {
        vector<int> vec(58,0);
        for(auto& s : str)
            vec[s - 'A'] += 1;
        for(int i = 0;i<str.size();i++){
            if(vec[str[i] - 'A'] == 1) return i;
        }
        return -1;
    }
};

法二

如果记不清ASCII码最小的字符是A还是a,直接用哈希表来存也是一样的,只需要一个简单的替换。

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

35. 数组中的逆序对

数组中的逆序对_牛客题霸_牛客网 (nowcoder.com)

法一

直接暴力,果不其然超时了。

class Solution {
public:
    int InversePairs(vector<int>& nums) {
        int ans = 0;
        const int MOD = 1e9 + 7;
        for(int i = 0;i<nums.size();i++){
            for(int j = i + 1;j<nums.size();j++){
                if(nums[j] < nums[i]){
                    ans++;
                    ans %= MOD;
                }
            }
        }
        return ans;
    }
};

法二

归并排序与逆序对息息相关。

求nums的逆序对时,将nums从中间分为两部分。故nums的逆序对=(1)左半数组的逆序对 + (2)右半数组逆序对 + (3)左右两边的逆序对(右边数组中的数<左边数组时)。

  • 其中(1)和(2)可以直接递归去算,直到递归出口(数组中只有一个元素,逆序对为0)。
  • (3)则需要在归并左右两个数组时进行计算。设排序时,左右两个数组当前索引分别为i,j,中间值为mid。此时只有j处的元素 < i处的元素时,会产生逆序对(从i开始到mid处,即mid - i + 1个)。
class Solution {
public:
    int InversePairs(vector<int>& nums) {
        vector<int> copy(nums.size());
        return MergeSort(nums,copy,0,nums.size() - 1);
    }
private:
    const int MOD = 1e9 + 7;
    int MergeSort(vector<int>& data,vector<int>& copy,int left,int right){
        if(left >= right) return 0; // 递归出口数组只有一个元素,不存在逆序对
        // 递归,分为左右两组分别归并
        int mid = left + (right - left) / 2;
        int ans = MergeSort(data, copy, left, mid )+ MergeSort(data, copy, mid + 1, right);
        ans %= MOD;
        // 合并
        int i = left,j = mid + 1; // 左右两个排好序的子数组的起始索引
        for(int k = left;k<=right;k++) copy[k] = data[k]; //暂时记录[left,right]间的顺序,开始合并
        for(int k = left;k<=right;k++){
            if(i == mid + 1) data[k] = copy[j++]; // 左边数组合并完了
            else if(j == right + 1) data[k] = copy[i++]; // 右边数组合并完了
            else if(copy[i] <= copy[j]) data[k] = copy[i++]; // 左边数组当前值<=右边数组
            else{       // 左边数组当前值大于右边数组
                data[k] = copy[j++];
                ans += (mid - i + 1);  // 将右边数组当前值归位时,增加了 mid - i + 1个逆序对(左边数组剩余元素)
                ans %= MOD;
            }
        }
        return ans;
    }
};

法三

  • 法二在合并两个数组的过程中,设计到很多数据交换。例如,首先要将数据从data写入到copy,然后一边排序一边写回data。
  • 可以换一种思路实现,每次都将data中的数据排序后写入到copy。
  • 该方法交换次数减少,但可读性降低,慎用。
class Solution {
public:
    int InversePairs(vector<int>& nums) {
        vector<int> copy(nums);
        return MergeSort(nums,copy,0,nums.size()-1);
    }
private:
    const int MOD = 1e9 + 7;
    int MergeSort(vector<int>& data,vector<int>& copy,int left,int right){
        if(left >= right) return 0;
        int mid = left + (right - left) / 2;
        int ans = MergeSort(copy, data, left, mid) + MergeSort(copy,data, mid + 1, right);
        ans %= MOD;
        int i = left,j = mid + 1;
        for(int k = left;k<=right;k++){
            if(i == mid + 1) copy[k] = data[j++];
            else if(j == right + 1 || data[i] <= data[j]) copy[k] = data[i++];
            else {
                copy[k] = data[j++];
                ans += (mid + 1 - i);
                ans %= MOD;
            }
        }
        return ans;
    }
};

注:

  • data理解为原有乱序数组,copy理解为排好序的数组,归并过程就是要将data中的数据排序后放在copy中。即该法的思路是将data中的数据排序后放在copy中,表现在代码中就是与法二相比归并过程中copy成为了左值
  • 在每一次递归调用中,datacopy 两个数组的角色在递归间调换。一个数组在一次递归中作为源数组,而在下一次递归中作为目标数组。这样,每一次递归都是将有序的部分从一个数组复制到另一个数组中,而无需进行元素的实际交换。
  • 由于该方法中copy数组不需要一次次的交换数据,故一开始其数据必须和nums一致。vector<int> copy(nums);不可以用 vector<int> copy(nums.size());代替。
  • MergeSort(copy, data, left, mid)该函数中copy与data参数交换了一下。
  • if(j == right + 1 || data[i] <= data[j])由于两个条件的执行语句一样将其放在一起写了。

36. 两个链表的第一个公共结点

两个链表的第一个公共结点_牛客题霸_牛客网 (nowcoder.com)

法一

需要找的是公共节点(该节点之后两条链表变为一条),要与值相同的链表区分。

  1. 计算两条链表的长度,cnt1和cnt2。
  2. 将两条链表末尾对齐,(长的链表往后走diff(|cnt1 - cnt2|)步)。因为如果存在公共节点的话一定是从一个交汇节点到链表末尾都一样。
  3. 开始遍历对齐后的链表,找到一样的返回,找不到表示不存在。
/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
		ListNode* cur1 = pHead1;
		ListNode* cur2 = pHead2;
		int cnt1 = 0,cnt2 = 0;
		while(cur1){
			cur1 = cur1->next;
			cnt1++;
		}
		while(cur2){
			cur2 = cur2->next;
			cnt2++;
		}
		cur1 = pHead1,cur2 = pHead2;
		if(cnt1 < cnt2){
			swap(cur1,cur2);
			swap(cnt1,cnt2);
		}
		int diff = cnt1 - cnt2;
		while(diff--) cur1 = cur1->next;
		while(cur1 && cur2){
			if(cur1 == cur2) return cur1;
			cur1 = cur1->next;
			cur2 = cur2->next;
		}
		return nullptr;
    }
};

法二

该题最大的问题是在于两条链表的长度可能不同,

  • 若两条链表长度相同,则一个循环直接边遍历边比较即可,找不到就是不存在。
  • 若长度不同,可以在指针到了链表结尾后指向另一条链表的头。这样便抹平了长度差(两个链表上的所有元素都要被指针走一趟),这样结束的时候要么两个指针都指向nullptr,要么都指向公共节点。
class Solution {
public:
    ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
        ListNode* cur1 = pHead1;
				ListNode* cur2 = pHead2;
				while(cur1 != cur2){
					cur1 = (cur1 == nullptr ? pHead2 : cur1->next);
					cur2 = (cur2 == nullptr ? pHead1 : cur2->next);
				}
				return cur1;
    }
};

37.统计一个数字在排序数组中出现的次数

数字在升序数组中出现的次数_牛客题霸_牛客网 (nowcoder.com)

法一

很直接的想法,题目怎么说,我就怎么来。

class Solution {
public:
    int GetNumberOfK(vector<int>& nums, int k) {
        int cnt = 0;
        for(auto& i : nums){
            if(i == k) cnt++;
        }
        return cnt;
    }
};

法二

由于是非降序数组,可以利用二分法寻找k的后一个元素和k的前一个元素的索引位置,两者相减就是k出现的次数。

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

注:

  • 自己写的二分查找中k需要用浮点数,否则无法通过加减0.5找到与k相连的元素。
  • 二分查找过程中不需要考虑data[mid]==k,因为k一定不是整数,而data数组里都是整数,两者不会相等。
  • BinarySearch返回值返回leftright都可。因为所求结果是一个差值,而不是一个精确的位置,在跳出while循环时,right永远比left大1。

38. 二叉树的深度

二叉树的深度_牛客题霸_牛客网 (nowcoder.com)

法一

树的问题归根结底就是遍历问题,求深度可以用层序遍历,遍历一层cnt加一即可。

class Solution {
public:
    int TreeDepth(TreeNode* pRoot) {
		int cnt = 0;
		queue<TreeNode*> que;
		if(pRoot != nullptr) que.push(pRoot);
		while(!que.empty()){
			int size = que.size();
			cnt++;
			for(int i = 0;i<size;i++){
				TreeNode* node = que.front();
				que.pop();
				if(node->left) que.push(node->left);
				if(node->right) que.push(node->right);
			}
		}
		return cnt;
    }
};

法二

递归的逻辑也很容易梳理出来

递归出口为根节点为0,return 0。

递归公式为,一棵树的深度等于左右子树的最大深度 + 1。

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

39. 判断是不是平衡二叉树

判断是不是平衡二叉树_牛客题霸_牛客网 (nowcoder.com)

法一

一个很直观的想法就是递归。平衡二叉树就是任何子树的左右子树之间的深度差小于等于1。结合38题的代码,遍历一遍节点看看是否符合条件即可。

class Solution {
public:
    int MaxDepth(TreeNode* root){
        if(root == nullptr) return 0;
        return max(MaxDepth(root->left),MaxDepth(root->right)) + 1;
    }
    bool IsBalanced_Solution(TreeNode* root) {
        if(root == nullptr) return true;
        return abs(MaxDepth(root->left) - MaxDepth(root->right)) <= 1 && IsBalanced_Solution(root->left) && IsBalanced_Solution(root->right);
    }
};

法二

法一的冗余计算太多了,在遍历上层节点时会多次重复遍历下层的节点。可以更换一种遍历方式,由下往上遍历。遍历过程中,若该子树的平衡树返回树的深度,不是则返回-1。

class Solution {
public:
    int GetDepth(TreeNode* root){
        if(root == nullptr) return 0;
        int left = GetDepth(root->left);   // 左
        if(left == -1) return -1;  // 左子树不是平衡树
        int right = GetDepth(root->right);  // 右
        if(right == -1) return -1;  // 右子树不是平衡树
        if(abs(left - right) > 1) return -1;  //不是平衡树,中
        return max(left,right) + 1;  
    }
    bool IsBalanced_Solution(TreeNode* root) {
        if(root == nullptr) return true;
        return GetDepth(root)!= -1;
    }
};

40. 数组中只出现一次的数字

数组中只出现一次的数字_牛客题霸_牛客网 (nowcoder.com)

法一

初看此题有点惊讶,因为要找到两个数字,而c++不能直接返回两个数,所以给定参数采用了两个指针来保存结果,这也是需要返回多个结果时的一种常见方式。

第一印象还是直接按照题目说的来,题目怎么说我就怎么写。遍历一遍数一数每个数字出现了几次,然后再遍历一次,数两个只出现一次的就可以了。

class Solution {
public:
    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
        unordered_map<int, int> mp;
        for(auto& num : data) mp[num]++;
        auto it = mp.begin();
        while(it != mp.end()){
            if(it->second == 1){
                *num1 = it->first;
                it++;
                break;
            }
            it++;
        }
        while(it != mp.end()){
            if(it->second == 1){
                *num2 = it->first;
                it++;
                break;
            }
            it++;
        }
        return ;
    }
};

法二

A ^ A = 0,所以如果只有一个数字出现了一次的话,直接把数组中的数全异或一次就得到结果了。但该题里右两个那么全异或一次,得到的结果就是A ^ B。该数的1表示A与B不同的位,0表示相同的位。

所以,可以知道A和B的哪一位是不同的。根据这一点将数组分为两组。每组里面自己全部异或一遍得到的就是两个不同的数。

class Solution {
public:
    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
        int totalNum = 0;
        for(auto& num : data) totalNum ^= num;
        int sign = 0;
				// get A ^ B
        while(totalNum){
            if(totalNum & (1 << sign)) break;
            sign++;
        }
        num1[0] = 0;
        num2[0] = 0;
        for(auto& i :  data){   // 分组异或
            if(i & (1 << sign)){
                num1[0] ^= i;
            }else{
                num2[0] ^= i;
            }
        }
        return ;
    }
};
  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

记与思

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

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

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

打赏作者

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

抵扣说明:

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

余额充值