美文网首页
剑指offer学习笔记:5.1 面试官谈效率 && 5.2 时间

剑指offer学习笔记:5.1 面试官谈效率 && 5.2 时间

作者: 小逗比儿 | 来源:发表于2020-07-03 21:52 被阅读0次

面试题29:数组中出现次数超过一半的数字(解法巨多的一道题,leetcode官方就给了5个解法)

数组中有一个数字出现次数超过数组长度的一半,请找出这个数字
leetcode链接 https://leetcode-cn.com/problems/majority-element/

class Solution {
public:
   int majorityElement(vector<int>& nums) {
      //  这里写的是投票法,思路见下面解法二
       int candidate = nums[0];
       int n = 1;
       for(int i = 1; i < nums.size(); i++)
       {
           if (nums[i] == candidate)
           {
               n++;
           }
           else
           {
               n--;
               if (n == 0)
               {
                   candidate = nums[i];
                   n = 1;
               }
           }
       }
       int num = 0;
       for(int i = 0; i < nums.size(); i++)
       {
           if (nums[i] == candidate)
           {
               num++;
           }
       }
       if (num > nums.size() / 2)
       {
           return candidate;
       }
       else{
           return 0;
       }
   }
};

解题思路:只写了剑指offer书上的两种方法,排序法和投票法。但是投票法我感觉只在众数一定存在的情况下适用,没有兼容异常的能力。
最简单直观的方法:简历hash表存数字和数字出现次数,当某个数字出现次数大于n/2,则return。
解法1:因为数组中超过一半的数字一定是数组中位数,同找数组中第k大元素算法。算法启蒙是快速排序。随机选择一个数,将比他小的数都调整到其左面,比他大的数都调整到其右面。当当前选中数据下标为n/2, 为中位数,当下标小于n/2,在他的右数组中查找,当大于,在左数组中查找。知道找到n/2位置元素。
注意考虑输入是null和没有超过一半数字的情况。
leetcode上用这种算法会超出时间限制,改为在牛客链接上写https://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163?tpId=13&&tqId=11181&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

class Solution {
public:
        void swap(int &a, int &b)
    {
        int tmp = a;
        a = b;
        b = tmp;
    }
    int partation(vector<int>& num, int begin, int end)
    {
        if (begin >= end)
        {
            return begin;
        }
        int target = num[begin];
        while(begin < end)
        {
            while(begin < end && num[end] >= target)
            {
                end--;
            }
            swap(num[begin], num[end]);
            while(begin < end && num[begin] <= target)
            {
                begin++;
            }
            swap(num[begin], num[end]);
        }
        return begin;
    }
    int quickSort(vector<int>& nums, int begin, int end)
    {
        if (begin >= end)
        {
            return nums[begin];
        }
        int pivot = partation(nums, begin, end);
        // cout << pivot << endl;
        if (pivot == -1)
        {
            return -1;
        }
        else if (pivot == nums.size() / 2)
        {
            return nums[pivot];
        }
        else if (pivot > nums.size() / 2)
        {
            return quickSort(nums, begin, pivot - 1);
        }
        return quickSort(nums, pivot + 1, end);
    }
    int MoreThanHalfNum_Solution(vector<int> numbers) {
                if (numbers.size() == 0)
        {
            return -1;
        }
        // 先写个快排,然后找中位数
        int mid = quickSort(numbers, 0, numbers.size() - 1);
        int num = 0;
        for(int i = 0; i < numbers.size(); i++)
        {
            if (numbers[i] == mid)
            {
                num++;
            }
        }
        if (num > numbers.size() / 2)
        {
            return mid;
        }
        else{ return 0;}
    }
};

解法2:考虑数组中出现次数超过一半的数,其出现次数一定大于其余所有数字出现次数之后。因此,进行操作,用result记录当前数字,num=1。当当前数字与result相同num+1,当当前数字与result不同,result-1。num减为0后result更换为当前数字,并且num重新置1。最后result就是要得到的数字。
模拟23324522225

步骤 操作 result num
1 2 2 1
2 3 != 2 3 1
3 3 == 3 3 2
4 2 != 3 3 1
5 4 != 2 4 1
6 5 != 4 5 1
7 2 != 5 2 1
8 2 == 2 2 2
9 2 == 2 2 3
10 2 == 2 2 4
11 5 != 2 2 3
class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int candidate = nums[0];
        int n = 1;
        for(int i = 1; i < nums.size(); i++)
        {
            if (nums[i] == candidate)
            {
                n++;
            }
            else
            {
                n--;
                if (n == 0)
                {
                    candidate = nums[i];
                    n = 1;
                }
            }
        }
        return candidate;
    }
};

面试题30:最小的k个数

输入n个整数,找到其中最小的k个数
leetcode链接 https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/

class Solution {
public:
   int partition(vector<int>& arr, int begin, int end)
   {
       int target = arr[begin];
       end--;
       while(begin < end)
       {
           // cout << "begin: " << begin << " end: " << end << endl;
           while(begin < end && arr[end] >= target)
           {
               end--;
           }
           int tmp = arr[end];
           arr[end] = arr[begin];
           arr[begin] = tmp;
           while(begin < end && arr[begin] <= target)
           {
               begin++;
           }
           tmp = arr[end];
           arr[end] = arr[begin];
           arr[begin] = tmp;
       }
       return begin;
   }
   void quickSort(vector<int>& arr, int begin, int end, int k)
   {
       if (begin >= end)
       {
           return;
       }
       int pivot = partition(arr, begin, end);
       if (pivot == k)
       {
           return;
       }
       if (pivot > k)
       {
           quickSort(arr, begin, pivot, k);
       }
       else
       {
           quickSort(arr, pivot + 1, end, k);
       }
       return;
   }
   vector<int> getLeastNumbers(vector<int>& arr, int k) {
       // 快排思想,对应下文中第二种思路
       if (arr.size() < k)
       {
           return arr;
       }
       quickSort(arr, 0, arr.size(), k);
       vector<int> result;
       for(int i = 0; i < k; i++)
       {
           result.push_back(arr[i]);
       }
       return result;
   }
};

解题思路:1)最直观的思路,排序,取前k个,注意数组长度。2)与上题中解法一思想类似,用快排中partiton函数找到位于第k位的元素,在数组左面的部分就是最先的k个数。但是利用这种思想的问题就是需要改变原始数组。3)维护一个大小为k的数据结构,每次新增数据和当前最大值比,如果大于最大值,则不变。小于最大值,则替换。因为需要获得每次的最大值,第一想到的是大顶堆。但是因为大顶堆不好写,可以退而求其次选择查找复杂度为logk的红黑树。stl中set和multiset都是红黑树实现的,因此可以直接拿来用。

性能上来看第二种算法最好,但是因为要读进来全部数据并会对原始数组进行更改,某些场景尤其是数据量级很大时不适用。第三种算法只需维护k的数据结构就好,更适合海量数据取前k。
解法一:快排,取k

class Solution {
public:
    int partition(vector<int>& arr, int begin, int end)
    {
        int target = arr[begin];
        end--;
        while(begin < end)
        {
            // cout << "begin: " << begin << " end: " << end << endl;
            while(begin < end && arr[end] >= target)
            {
                end--;
            }
            int tmp = arr[end];
            arr[end] = arr[begin];
            arr[begin] = tmp;
            while(begin < end && arr[begin] <= target)
            {
                begin++;
            }
            tmp = arr[end];
            arr[end] = arr[begin];
            arr[begin] = tmp;
        }
        return begin;
    }
    void quickSort(vector<int>& arr, int begin, int end)
    {
        if (begin >= end)
        {
            return;
        }
        int pivot = partition(arr, begin, end);
        quickSort(arr, begin, pivot);
        quickSort(arr, pivot + 1, end);
        return;
    }
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        // 快排,之后取前k个数
        if (arr.size() < k)
        {
            return arr;
        }
        quickSort(arr, 0, arr.size());
        vector<int> result;
        for(int i = 0; i < k; i++)
        {
            result.push_back(arr[i]);
        }
        return result;
    }
};

解法二:用快排的partition

class Solution {
public:
    int partition(vector<int>& arr, int begin, int end)
    {
        int target = arr[begin];
        end--;
        while(begin < end)
        {
            // cout << "begin: " << begin << " end: " << end << endl;
            while(begin < end && arr[end] >= target)
            {
                end--;
            }
            int tmp = arr[end];
            arr[end] = arr[begin];
            arr[begin] = tmp;
            while(begin < end && arr[begin] <= target)
            {
                begin++;
            }
            tmp = arr[end];
            arr[end] = arr[begin];
            arr[begin] = tmp;
        }
        return begin;
    }
    void quickSort(vector<int>& arr, int begin, int end, int k)
    {
        if (begin >= end)
        {
            return;
        }
        int pivot = partition(arr, begin, end);
        if (pivot == k)
        {
            return;
        }
        if (pivot > k)
        {
            quickSort(arr, begin, pivot, k);
        }
        else
        {
            quickSort(arr, pivot + 1, end, k);
        }
        return;
    }
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        // 快排,之后取前k个数
        if (arr.size() < k)
        {
            return arr;
        }
        quickSort(arr, 0, arr.size(), k);
        vector<int> result;
        for(int i = 0; i < k; i++)
        {
            result.push_back(arr[i]);
        }
        return result;
    }
};

解法三:利用set的红黑树结构,实现每次获取最大值的操作。注意用multiset不是set,不然会被去重。这种方法更适合大数据,时间上比前两种要耗时。

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        if (arr.size() < k)
        {
            return arr;
        }
        // 红黑树,我来了
        // c++中set的底层实现为红黑树
        multiset<int, greater<int>> tmp;
        int i = 0;
        while(i < k)
        {
            tmp.insert(arr[i]);
            i++;
        }
        while(i < arr.size())
        {
            if (arr[i] < *tmp.begin())
            {
                tmp.erase(tmp.begin());
                tmp.insert(arr[i]);
            }
            i++;
        }
        vector<int> result;
        set<int>::iterator it;
        for(it = tmp.begin(); it != tmp.end(); it++)
        {
            result.push_back(*it);
        }
        return result;
    }
};

【c++拾遗】set与红黑树
参考链接:https://www.cnblogs.com/caiyishuai/p/8646345.html
C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。
set关联式容器。set作为一个容器也是用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。应该注意的是set中数元素的值不能直接被改变。
set是自动排序的,默认是从小到大排序,即取第一个元素就是最小值。使用multiset<int, greater<int> > 定义就是由大到小排序。当需要使用greater<int>时,在头文件里需要添加#include<functional>

面试题31:连续子数组的最大和

**输入一个整型数组,数组中有正数也有负数。数组中一个或连续多个正数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为o(n)。

class Solution {
public:
   int maxSubArray(vector<int>& nums) {
       if (nums.size() == 0)
       {
           return INT_MIN;
       }
       if (nums.size() == 1)
       {
           return nums[0];
       }
       int sum = nums[0], max = nums[0];
       for(int i = 1; i < nums.size(); i++)
       {
           if (sum > max)
           {
               max = sum;
           }
           // cout << max << endl;
           if (sum < 0)
           {
               sum = nums[i];
           }
           else
           {
               sum = sum + nums[i];
           }
       }
       if (sum > max)
       {
           max = sum;
       }
       return max;
   }
};

解题思路:
解法一:找规律考虑数组{1, -2, 3, 10, -4, 7, 2, -5}。在遇到前面数组的和为负数情况下,舍弃前面所有数据,开始重新累计。遇到负数情况下,记录当前子数组和,和最大值进行比较,若大于最大值,替换,小于,舍弃,继续执行。写代码可以简化,因为当前数据是负数,相加肯定会让sum变小,所以每步判断当前sum和max即可,不需要判断当前数据是否为负数。

步骤 操作 累加的子数组和 最大的子数组和
1 加1 1 1
2 加-2 -1(负数,舍弃) 1
3 加3 3 3
4 加10 13 13
5 加-4 9 13(负数,不更新最大值)
6 加7 16 16
7 加2 18 18
8 加-5 13 18(负数,不更新最大值)
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if (nums.size() == 0)
        {
            return INT_MIN;
        }
        if (nums.size() == 1)
        {
            return nums[0];
        }
        int sum = nums[0], max = nums[0];
        for(int i = 1; i < nums.size(); i++)
        {
            if (sum > max)
            {
                max = sum;
            }
            // cout << max << endl;
            if (sum < 0)
            {
                sum = nums[i];
            }
            else
            {
                sum = sum + nums[i];
            }
        }
        if (sum > max)
        {
            max = sum;
        }
        return max;
    }
};

解法二:应用动态规划
用函数f(i)表示到以第i个元素结尾的最大子数组的数字和。
f(i)= \begin{cases} \cf data[i], &i=0 或者 f(i-1) <= 0\\ f(i-1) + data[i], &i \neq 0 并且 f(i-1) > 0 \end{cases}

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if (nums.size() == 0)
        {
            return INT_MIN;
        }
        if (nums.size() == 1)
        {
            return nums[0];
        }
        int n = nums.size();
        int sum[n];
        sum[0] = nums[0];
        int max = nums[0];
        // 动态规划,用数组存结果
        for(int i = 1; i < n; i++)
        {
            if (sum[i-1] < 0)
            {
                sum[i] = nums[i];
            }
            else
            {
                sum[i] = sum[i-1] + nums[i];
            }
            if (sum[i] > max)
            {
                max = sum[i];
            }
        }
        return max;
    }
};

面试题32:从1到n整数中1出现的次数

输入一个整数n,求1到n这n个整数中十进制表示1出现的次数。例如输入12,从1到12包含1的数字有1,10,11,12,1一共出现了5次。
leetcode链接 https://leetcode-cn.com/problems/1nzheng-shu-zhong-1chu-xian-de-ci-shu-lcof/

class Solution {
public:
   int countNumber1(const char* strN)
   {
       if (!strN || *strN > '9' || *strN < '0' || *strN == '\0' || atoi(strN) < 1)
       {
           return 0;
       }
       int first = *strN - '0';
       unsigned int len = strlen(strN);
       if (first == 0 && len == 1)
       {
           return 0;
       }
       if (first > 0 && len == 1)
       {
           return 1;
       }
       int high;    // 最高位是1的个数
       if (first > 1)
       {
           high = pow(10, len - 1);
       }
       else{
           high = atoi(strN + 1) + 1;
       }
       // cout << first << " " << len << endl;
       int mid = first * (len - 1) * pow(10, len - 2);// 其余位是1的个数是个排列组合
       int i = 1;
       while(*(strN + i) == '0')  //  感觉书上写的那个缺这部分去0,譬如101这种,直接+1传过去的是01
       {
           i++;
       }
       int low = countNumber1(strN + i);  // 递归剩余部分
       // cout << high << " " << mid << " " << low << endl;
       return high + mid + low;
   }
   int countDigitOne(int n) {
       if (n == 0)
       {
           return 0;
       }
       if (n == 1)
       {
           return 1;
       }
       // 涉及最高位删除,转换成字符串操作比较方便
       char strN[50];
       sprintf(strN, "%d", n);
       return countNumber1(strN);
   }
};

解题思路:从数据规律着手。考虑11-110中1出现的个数,百位出现次数(1)+ 10位出现次数10-19(10)+ 个位出现次数11,21,31...101(10)= 21个。考虑15-114中1出现的个数,百位出现次数100-114(15)+ 10位出现次数15-19,110-114(10)+ 个位出现次数21-91,101,111(10)= 35个。115-214同上。发现除了最高位不同,百位和十位都是相同的,其个数为排列组合,固定一个值是1,其余另一位可以取0-9共10个数,所以是位数*10^{(位数-1)}
取一个比较大的数进行分析,譬如取21345,将数据分为两段,1-1345,1346-21345。由前面判断,可得到1346-21345中1的个数=最高位中1的个数10000-19999(10000)+ 其余位中1的个数1346-11345,11346-21346(2410^3 = 8000) = 18000, 1-1345中1的个数,同样需要一步一步拆分,可以用递归来实现。

面试题33:把数组排成最小的数

输入一个正整数数组,把数组中的所有数字拼接排列成一个数,打印能排列出的数字中最小的一个。例如输入数组{3, 32, 321}, 则打印出这3个数字能排列成的最小数字是321323。
leetcode链接 https://leetcode-cn.com/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/

class Solution(object):
   def compare(self, a, b):
       # print "a: " + a + " b: " + b + " ",
       if (a+b) < (b+a):
           # print "-1"
           return -1
       elif (a+b) > (b+a):
           # print "1"
           return 1
       # print "0"
       return 0
   def minNumber(self, nums):
       """
       :type nums: List[int]
       :rtype: str
       """
       # 先将int转string
       for i in range(0, len(nums)):
           nums[i] = str(nums[i])
       nums = sorted(nums, cmp = self.compare)
       result = ""
       for i in range(0, len(nums)):
           result += nums[i]
       return result

解题思路:将数字转换成字符串,按从小到大排序,之后组合即可。注意排序规则是字符串x和字符串y,若x+y>y+x,则x>y。用sort自定义函数的功能进行排序。这种转字符串的python比较简单,用python写了。

第二版面试题44:数字序列中某一位数字

数字以012345678910111213排列。在这个序列中,第5位是5(从0开始计数),第13位是1。实现函数,求第n位的数字
leetcode链接 https://leetcode-cn.com/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof/

class Solution {
public:
   int countDigit(int n)
   {
       // 计算几位数都有几个数字
       if (n == 1)
       {
           return 10;
       }
       return pow(10, n) - pow(10, n - 1);
   }
   int findNthDigit(int n) {
       // 先确定范围
       int digits = 1;
       while(true)
       {
           int tmp = countDigit(digits);
           if (tmp >= int(INT_MAX / digits))   // 这里一定要注意下判断越界问题
           {
               break;
           }
           int num = tmp * digits;
           if (n < num)
           {
               break;
           }
           n = n - num;
           digits++;
       }
       // cout << n << " " << digits << endl;
       if (digits == 1)
       {
           return n;
       }
       int wei = n / digits;
       int mid = n % digits;
       // cout << "wei: " << wei << " mid: " << mid << endl;
       int result = pow(10, digits - 1 ) + wei;
       // cout << "result: " << result << endl;
       return (result / int(pow(10, digits - 1 - mid))) % 10;
   }
};

解题思路:由于一位数,两位数,三位数占用长度都是固定的,可以先求得n位数所在范围。以1001位为例,由于0-9占用前10位,只有一个数字,1001显然在后面,因此找10开始第991个数字。由于10-99占902=180位,不够991,因此找100开始第811位。接下来100-999=9003=2700位,则1001在这里面。由于811=270*3 + 1。因此1001是从100开始第270个数字,即370中间的那位,即为7。

相关文章

网友评论

      本文标题:剑指offer学习笔记:5.1 面试官谈效率 && 5.2 时间

      本文链接:https://www.haomeiwen.com/subject/wwbmfktx.html