剑指offer31-40

31题目描述

整数中1出现的次数(从1到n整数中1出现的次数)
求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

思路

注意审题,1出现的次数,不是包含1的数有多少个。 问题不要想复杂了,使用简单的模10即可解决。

代码

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

32题目描述

把数组排成最小的数:
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

思路

用vector自带sort函数进行排序,顺序由自己重新定义:a b转换成string后进行比较,ab<ba则保持顺序不变,ab>ba表示后面的要放到前面。这里需要熟悉vector的sort函数以及to_string,string之间的比较。https://blog.csdn.net/ihadl/article/details/7400929

代码

class Solution {
public:
    static bool exchange(int i, int j)
    {
        string A = "";
        string B = "";
        A += to_string(i);
        A += to_string(j);
        B += to_string(j);
        B += to_string(i);
        return A < B;
    }
    
    string PrintMinNumber(vector<int> numbers) 
    {
        string result = "";
        if(numbers.empty()) return result;
        sort(numbers.begin(), numbers.end(), exchange);
        for(int i = 0; i < numbers.size(); i++)
            result += to_string(numbers[i]);
        return result;
    }
};

33题目描述

丑数:
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

思路

链接:https://www.nowcoder.com/questionTerminal/6aa9e04fc3794f68acf8778237ba065b 来源:牛客网

通俗易懂的解释: 首先从丑数的定义我们知道,一个丑数的因子只有2,3,5,那么丑数p = 2 ^ x * 3 ^ y * 5 ^ z,换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到,那么我们从1开始乘以2,3,5,就得到2,3,5三个丑数,在从这三个丑数出发乘以2,3,5就得到4,6,10,6,9,15,10,15,25九个丑数,我们发现这种方***得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护三个队列:
(1)丑数数组: 1
乘以2的队列:2
乘以3的队列:3
乘以5的队列:5
选择三个队列头最小的数2加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(2)丑数数组:1,2
乘以2的队列:4
乘以3的队列:3,6
乘以5的队列:5,10
选择三个队列头最小的数3加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(3)丑数数组:1,2,3
乘以2的队列:4,6
乘以3的队列:6,9
乘以5的队列:5,10,15
选择三个队列头里最小的数4加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(4)丑数数组:1,2,3,4
乘以2的队列:6,8
乘以3的队列:6,9,12
乘以5的队列:5,10,15,20
选择三个队列头里最小的数5加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(5)丑数数组:1,2,3,4,5
乘以2的队列:6,8,10,
乘以3的队列:6,9,12,15
乘以5的队列:10,15,20,25
选择三个队列头里最小的数6加入丑数数组,但我们发现,有两个队列头都为6,所以我们弹出两个队列头,同时将12,18,30放入三个队列;
…………………… 疑问:
1.为什么分三个队列?
丑数数组里的数一定是有序的,因为我们是从丑数数组里的数乘以2,3,5选出的最小数,一定比以前未乘以2,3,5大,同时对于三个队列内部,按先后顺序乘以2,3,5分别放入,所以同一个队列内部也是有序的;
2.为什么比较三个队列头部最小的数放入丑数数组?
因为三个队列是有序的,所以取出三个头中最小的,等同于找到了三个队列所有数中最小的。
实现思路:
我们没有必要维护三个队列,只需要记录三个指针显示到达哪一步;“|”表示指针,arr表示丑数数组;
(1)1
|2
|3
|5
目前指针指向0,0,0,队列头arr[0] * 2 = 2, arr[0] * 3 = 3, arr[0] * 5 = 5
(2)1 2
2 |4
|3 6
|5 10
目前指针指向1,0,0,队列头arr[1] * 2 = 4, arr[0] * 3 = 3, arr[0] * 5 = 5
(3)1 2 3
2| 4 6
3 |6 9
|5 10 15
目前指针指向1,1,0,队列头arr[1] * 2 = 4, arr[1] * 3 = 6, arr[0] * 5 = 5

代码

class Solution {
public:
    int GetUglyNumber_Solution(int index) {
        if (index < 7)return index;
        vector<int> res(index);
        res[0] = 1;
        int t2 = 0, t3 = 0, t5 = 0, i;
        for (i = 1; i < index; ++i)
        {
            res[i] = min(res[t2] * 2, min(res[t3] * 3, res[t5] * 5));
            if (res[i] == res[t2] * 2)t2++;
            if (res[i] == res[t3] * 3)t3++;
            if (res[i] == res[t5] * 5)t5++;
        }
        return res[index - 1];
    }
};

34题目描述

第一个只出现一次的字符
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)。

思路1

建立两个辅助数组和一个辅助字符串。第一个辅助数组count用来记录每个字符出现的次数,第二个辅助数组address用来记录字符出现的地址,辅助字符串用来记录出现的字符。三者在位置上对应。遍历整个字符串,直接将出现的字符相应属性记录在辅助数组与字符串中即可。 最后遍历count找到第一个为1的index,其对应的address就是这个字符的位置。

代码

class Solution {
public:
    int FirstNotRepeatingChar(string str) 
    {
        int flag = 0;
        vector<int> count;
        vector<char> character;
        vector<int> address;
        for(int i = 0; i < str.length(); i++)
        {
            flag = 0;
            if(count.empty()) 
            {
                count.push_back(1);
                character.push_back(str[i]);
                address.push_back(i);
                continue;
            }
            for(int j = 0; j < character.size(); j++)
            {
                if(str[i] == character[j])
                {
                    count[j]++;
                    flag = 1;
                }
            }
            if(flag) continue;
            count.push_back(1);
            character.push_back(str[i]);
            address.push_back(i);
        }

        for(int i = 0; i < count.size(); i++)
        {
            if(count[i] == 1) return address[i];
        }
        return -1;
    }
};

思路2

使用STL的map(1对1关联容器)记录, 更加方便。

代码

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

35题目描述

数组中的逆序数
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述:

题目保证输入的数组中没有的相同的数字

数据范围:

	对于%50的数据,size<=10^4

	对于%75的数据,size<=10^5

	对于%100的数据,size<=2*10^5

示例1:

输入
1,2,3,4,5,6,7,0
输出
7

思路

思路分析:
看到这个题目,我们的第一反应是顺序扫描整个数组。每扫描到一个数组的时候,逐个比较该数字和它后面的数字的大小。如果后面的数字比它小,则这两个数字就组成了一个逆序对。假设数组中含有n个数字。由于每个数字都要和O(n)这个数字比较,因此这个算法的时间复杂度为O(n^2)。
我们以数组{7,5,6,4}为例来分析统计逆序对的过程。每次扫描到一个数字的时候,我们不拿ta和后面的每一个数字作比较,否则时间复杂度就是O(n^2),因此我们可以考虑先比较两个相邻的数字。

(a) 把长度为4的数组分解成两个长度为2的子数组;
(b) 把长度为2的数组分解成两个成都为1的子数组;
(c) 把长度为1的子数组 合并、排序并统计逆序对 ;
(d) 把长度为2的子数组合并、排序,并统计逆序对;
在上图(a)和(b)中,我们先把数组分解成两个长度为2的子数组,再把这两个子数组分别拆成两个长度为1的子数组。接下来一边合并相邻的子数组,一边统计逆序对的数目。在第一对长度为1的子数组{7}、{5}中7大于5,因此(7,5)组成一个逆序对。同样在第二对长度为1的子数组{6}、{4}中也有逆序对(6,4)。由于我们已经统计了这两对子数组内部的逆序对,因此需要把这两对子数组 排序 如上图(c)所示, 以免在以后的统计过程中再重复统计。
接下来我们统计两个长度为2的子数组子数组之间的逆序对。合并子数组并统计逆序对的过程如下图如下图所示。
我们先用两个指针分别指向两个子数组的末尾,并每次比较两个指针指向的数字。如果第一个子数组中的数字大于第二个数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数,如下图(a)和(c)所示。如果第一个数组的数字小于或等于第二个数组中的数字,则不构成逆序对,如图b所示。每一次比较的时候,我们都把较大的数字从后面往前复制到一个辅助数组中,确保 辅助数组(记为copy) 中的数字是递增排序的。在把较大的数字复制到辅助数组之后,把对应的指针向前移动一位,接下来进行下一轮比较。

过程:先把数组分割成子数组,先统计出子数组内部的逆序对的数目,然后再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,还需要对数组进行排序。如果对排序算法很熟悉,我们不难发现这个过程实际上就是归并排序。

交换copy和data是因为:
1.在每次的操作中,数值的比较都是采用当前传入函数中第一项,也就是data;比较的结果都存放到copy中;也就意味着此时copy中是经过此次调用的结果。
2.从最底层返回时,进入了(start == end)的情形,data 和 copy 完全没有修改,此时copy和data还是一样的。
3.进入倒数第二层时,程序进入上图26行以后部分,copy是部分排序后的新数组,data是旧数组。注意这里都是传值的调用,数组都是直接修改的。
倒数第二层使用的copy其实是倒数第三层中的data,这就确保了倒数第三层进入26行以后时,数据比较使用的data是最新排序的数组。
4.倒数第三层将排序的结果存入copy中。程序在倒数第四层进入26行后,使用的data数组为刚刚倒数第三层中的最新排序的copy.
5.也就是说,在每次程序进入26行时,此时的data是最新的排序结果,copy是次新的结果。
在最后一次进入26行以后时,copy为完整排序后的结果,data是次新的结果。
然而这里第一个类内函数调用第二个函数时,data和copy的顺序没有改变,所以最后结果应该copy是完整排序的结果.data是差一步完成排序的结果。以输入[7,5,6,4], 最后的结果copy[4,5,6,7], data[5,7,4,6].

代码

class Solution {
public:
    int InversePairs(vector<int> data) 
    {
        if(data.empty()) return 0;
        vector<int> copy;
        for(int i = 0; i < data.size(); i++)
            copy.push_back(data[i]);
        int length = data.size();
        long long count = MergeSort(data, copy, 0, length - 1);
        return count % 1000000007;
    }
    long long MergeSort(vector<int>& data, vector<int>& copy, int start, int end)
    {
        if(start == end)
        {
            copy[start] = data[start];
            return 0; 
        }
        int mid = (end - start) / 2;
        long long left = MergeSort(copy, data, start, start + mid);
        long long right = MergeSort(copy, data, start + mid + 1, end);
        int left_point = start + mid;
        int right_point = end;
        int index_copy = end;
        long long count = 0;
        while(left_point >= start && right_point >= start + mid + 1)
        {
            if(data[left_point] > data[right_point])
            {
                count = count + (right_point - (start + mid + 1) + 1);
                copy[index_copy--] = data[left_point--];
            }
            else
            {
                copy[index_copy--] = data[right_point--];
            }
        }
        while(left_point >= start)
            copy[index_copy--] = data[left_point--];
        while(right_point >= start + mid + 1)
            copy[index_copy--] = data[right_point--];
        
        return count + left + right;
    }
};

36题目描述

两个链表的第一个公共结点
输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)

思路

本题首先要明白,公共结点代表着,两个链表拥有公共尾部(因为只有一个next)。 所以先计算两者长度,然后让长的先走两个链表长度差后 再一起走,当相同时,就是第一个公共结点。

代码

/*
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 == NULL || pHead2 == NULL) return NULL;
        int pHead1_len = 0;
        int pHead2_len = 0;
        ListNode* p1 = pHead1;
        ListNode* p2 = pHead2;
        while(p1 != NULL)
        {
            pHead1_len++;
            p1 = p1 -> next;
        }
        while(p2 != NULL)
        {
            pHead2_len++;
            p2 = p2 -> next;
        }
        p1 = pHead1;
        p2 = pHead2;
        int difference = 0;
        if(pHead1_len > pHead2_len)
        {
            difference = pHead1_len - pHead2_len;
            while(difference)
            {
                p1 = p1 -> next;
                difference--;
            }
        }
        if(pHead2_len > pHead1_len)
        {
            difference = pHead2_len - pHead1_len;
            while(difference)
            {
                p2 = p2 -> next;
                difference--;
            }
        }
        while(p1 != NULL)
        {
            if(p1 == p2) return p1;
            else
            {
                p1 = p1 -> next;
                p2 = p2 -> next;
            }
        }
        return NULL;
    }
};

37题目描述

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

思路1

我所采用的二分查找变形。使用二分查找,找到k出现的最左位置,再向后遍历,如果重复则加一 不重复则break;

代码

class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) 
    {
        if(data.empty()) return 0;
        int lower = get_lower(data, k);
        int count = 0;
        if(lower != -1)
        {
            while(lower <= data.size() - 1)
            {
                if(data[lower] == k) 
                {
                    count++;
                    lower++;
                    continue;
                }
                else break;
            }
        }
        return count;
    }
    int get_lower(vector<int> data, int k)
    {
        int start = 0;
        int end = data.size() - 1;
        int mid = (start + end) / 2;
        int lower = -1;
        while(start <= end)
        {
            if(k == data[start]) 
            {
                lower = start;
                break;
            }
            else if(k == data[mid]) 
            {
                lower = mid;
                end = mid - 1;
            }
            else if(k < data[mid]) end = mid - 1;
            else if(k > data[mid]) start = mid + 1;
            mid = (start + end) / 2;
        }
        return lower;
    }
};

思路2

由于数组有序,所以使用二分查找方法定位k的第一次出现位置和最后一次出现位置 (找到上界下界,此程序有bug,有特殊测试用例则无法通过 (还有一种情况不严谨,就是比如1,2,3,4,6,7寻找5,会返回6的下标但是此刻数组中不存在5))

代码

链接:https://www.nowcoder.com/questionTerminal/70610bf967994b22bb1c26f9ae901fa2
来源:牛客网

class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) {
        int lower = getLower(data,k);
        int upper = getUpper(data,k);
         
        return upper - lower + 1;
    }
     
    //获取k第一次出现的下标
    int getLower(vector<int> data,int k){
        int start = 0,end = data.size()-1;
        int mid = (start + end)/2;
         
        while(start <= end){
            if(data[mid] < k){
                start = mid + 1;
            }else{
                end = mid - 1;
            }
            mid = (start + end)/2;
        }
        return start;
    }
    //获取k最后一次出现的下标
    int getUpper(vector<int> data,int k){
         int start = 0,end = data.size()-1;
        int mid = (start + end)/2;
         
        while(start <= end){
            if(data[mid] <= k){
                start = mid + 1;
            }else{
                end = mid - 1;
            }
            mid = (start + end)/2;
        }
         
        return end;
    }
};

38题目描述

二叉树的深度
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

思路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)
    {
        if(pRoot == NULL) return 0;
        int depth = 0;
        depth = count(pRoot, depth);
        return depth;
    }
    
    int count(TreeNode* p, int depth)
    {
        depth++;
        int left_depth = depth;
        int right_depth = depth;
        if(p -> left != NULL)
            left_depth = count(p -> left, depth);
        if(p -> right != NULL)
            right_depth = count(p -> right, depth);
        return max(left_depth, right_depth);
    }
};

思路2

使用队列,进行层次遍历

代码

class Solution {
public:
    int TreeDepth(TreeNode* pRoot) {
        if (!pRoot) return 0;
        queue<TreeNode*> que;
        que.push(pRoot);int depth=0;
        while (!que.empty()) {
            int size=que.size();
            depth++;
            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 depth;
    }
};

39题目描述

平衡二叉树
输入一棵二叉树,判断该二叉树是否是平衡二叉树。

思路

使用递归,这里要注意平衡二叉树的定义,一个平衡二叉树中每一个单独的子树也是一个平衡二叉树。平衡二叉树:左子树右子树深度之差不超过1。
使用了两个递归,一个是判断当前子树左右深度是否超过1(是否是平衡二叉树), 另一个是计算深度。 如果其中一个子树不是平衡二叉树,则直接返回否。

代码

class Solution {
public:
    bool IsBalanced_Solution(TreeNode* pRoot) 
    {
        if(pRoot == NULL) return 1;
        return IsBalanced(pRoot, 1);
    }
    
    bool IsBalanced(TreeNode* pRoot, bool is)
    {
        if(is == false) return false;
        int right_depth = 0;
        int left_depth = 0;
        bool right_is = 1;
        bool left_is = 1;
        if(pRoot -> left != NULL)
        {
            left_depth = CountDepth(pRoot -> left, left_depth);
            left_is = IsBalanced(pRoot -> left, 1);
        }
        if(pRoot -> right != NULL)
        {
            right_depth = CountDepth(pRoot -> right, right_depth);
            right_is = IsBalanced(pRoot -> right, 1);
        }
        if(abs(right_depth - left_depth) > 1) is = 0;
        return is && right_is && left_is;
    }
    
    int CountDepth(TreeNode* pRoot, int depth)
    {
        depth++;
        int left_depth = depth;
        int right_depth = depth;
        if(pRoot -> left != NULL)
            left_depth = CountDepth(pRoot -> left, left_depth);
        if(pRoot -> right != NULL)
            right_depth = CountDepth(pRoot -> right, right_depth);
        return max(left_depth, right_depth);
    }
};

40题目描述

数组中只出现一次的数字
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

思路

首先我们考虑这个问题的一个简单版本:一个数组里除了一个数字之外,其他的数字都出现了两次。请写程序找出这个只出现一次的数字。
这个题目的突破口在哪里?题目为什么要强调有一个数字出现一次,其他的出现两次?我们想到了异或运算的性质:任何一个数字异或它自己都等于0 。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字,因为那些出现两次的数字全部在异或中抵消掉了。
有了上面简单问题的解决方案之后,我们回到原始的问题。如果能够把原数组分为两个子数组。在每个子数组中,包含一个只出现一次的数字,而其它数字都出现两次。如果能够这样拆分原数组,按照前面的办法就是分别求出这两个只出现一次的数字了。
我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其它数字都出现了两次,在异或中全部抵消掉了。由于这两个数字肯定不一样,那么这个异或结果肯定不为0 ,也就是说在这个结果数字的二进制表示中至少就有一位为1 。我们在结果数字中找到第一个为1 的位的位置,记为第N 位。现在我们以第N 位是不是1 为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第N 位都为1 ,而第二个子数组的每个数字的第N 位都为0 。
现在我们已经把原数组分成了两个子数组,每个子数组都包含一个只出现一次的数字,而其它数字都出现了两次。因此到此为止,所有的问题我们都已经解决。

代码

class Solution {
public:
    void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) 
    {
        if(data.size() < 2) return ;
        int temp = data[0];
        for(int i = 1; i < data.size(); i++)
            temp ^= data[i];
        if(temp == 0) return ;
        int index = 0;
        while((temp & 1) == 0)
        {
            temp = temp >> 1;
            index++;
        }
        *num1 = 0;
        *num2 = 0;
        for(int i = 0; i < data.size(); i++)
        {
            if(IsBit(data[i], index))
                *num1 ^= data[i];
            else
                *num2 ^= data[i];
        }
    }
    
    bool IsBit(int i, int index)
    {
        i = i >> index;
        return (i & 1);
    }
};
comments powered by Disqus
Copyright © 2024 RainPot