牛客Top101 8/22

BM46.最小的K的数

方法:优先队列

优先队列即PriorityQueue,是一种内置的机遇堆排序的容器,分为大顶堆与小顶堆,大顶堆的堆顶为最大元素,其余更小的元素在堆下方,小顶堆与其刚好相反。且因为容器内部的次序基于堆排序,因此每次插入元素时间复杂度都是�(���2�)O(log2​n),而每次取出堆顶元素都是直接取出。

思路:

要找到最小的k个元素,只需要准备k个数字,之后每次遇到一个数字能够快速的与这k个数字中最大的值比较,每次将最大的值替换掉,那么最后剩余的就是k个最小的数字了。

如何快速比较k个数字的最大值,并每次替换成较小的新数字呢?我们可以考虑使用优先队列(大根堆),只要限制堆的大小为k,那么堆顶就是k个数字的中最大值,如果需要替换,将这个最大值拿出,加入新的元素就好了。

1

2

3

4

5

//较小元素入堆

if(q.peek() > input[i]){ 

    q.poll();

    q.offer(input[i]);

}

具体做法:

  • step 1:利用input数组中前k个元素,构建一个大小为k的大顶堆,堆顶为这k个元素的最大值。
  • step 2:对于后续的元素,依次比较其与堆顶的大小,若是比堆顶小,则堆顶弹出,再将新数加入堆中,直至数组结束,保证堆中的k个最小。
  • step 3:最后将堆顶依次弹出即是最小的k个数。
class Solution {
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        vector<int> res;
        //排除特殊情况
        if(k == 0 || input.size() == 0) 
            return res;
        priority_queue<int> q; 
        //构建一个k个大小的堆 
        for(int i = 0; i < k; i++)
            q.push(input[i]);
        for(int i = k; i < input.size(); i++){
            //较小元素入堆
            if(q.top() > input[i]){  
                q.pop();
                q.push(input[i]);
            }
        }
        //堆中元素取出入vector
        for(int i = 0; i < k; i++){ 
            res.push_back(q.top());
            q.pop();
        }
        return res;
    }
};

 BM47.寻找第K大

方法:快排+二分查找(推荐使用)

知识点:分治

分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。

思路:

本题需要使用快速排序的思想,快速排序:每次移动,可以找到一个标杆元素,然后将大于它的移到左边,小于它的移到右边,由此整个数组划分成为两部分,然后分别对左边和右边使用同样的方法进行排序,不断划分左右子段,直到整个数组有序。这也是分治的思想,将数组分化成为子段,分而治之。

放到这道题中,如果标杆元素左边刚好有k−1个比它大的,那么该元素就是第k大:

1

2

3

//若从0开始,刚好是第K个点,则找到

if(K == j + 1

    return a[p];

如果它左边的元素比k−1少,说明第k大在其右边,直接二分法进入右边,不用管标杆元素左边:

1

2

if(j + 1 < k)

    return partition(a, j + 1, high, k);

同理如果它右边的元素比k−1少,那第k大在其左边,右边不用管。

1

return partition(a, low, j - 1, k);

具体做法:

  • step 1:进行一次快排,大元素在左,小元素在右,得到的标杆j点.在此之前要使用随机数获取标杆元素,防止数据分布导致每次划分不能均衡。
  • step 2:如果 j + 1 = k ,那么j点就是第K大。
  • step 3:如果 j + 1 > k,则第k大的元素在左半段,更新high = j - 1,执行step 1。
  • step 4:如果 j + 1 < k,则第k大的元素在右半段,更新low = j + 1, 再执行step 1.
class Solution {
public:
    int partition(vector<int>& a, int low, int high, int k){
        //随机快排划分
        swap(a[low], a[rand() % (high - low + 1) + low]);
        int v = a[low];
        int i = low + 1;
        int j = high;
        while(true){
            //小于标杆的在右
            while(j >= low + 1 && a[j] < v) 
                j--;
            //大于标杆的在左
            while(i <= high && a[i] > v) 
                i++;
            if(i > j) 
                break;
            swap(a[i], a[j]);
            i++;
            j--;
        }
        //把标杆元素置在最左
        swap(a[low], a[j]);
        //从0开始,所以为第j+1大
        if(j + 1 == k)
            return a[j];
        //继续划分,因为采用的是绝对位置,所以不需要k = (p - low + 1) 
        else if(j + 1 < k)
            return partition(a, j + 1, high, k);
        else
            return partition(a, low, j - 1, k);
    }
    int findKth(vector<int> a, int n, int K) {
        return partition(a, 0, n - 1, K);
    }
};

收获总结:

1.快速排序的重要性以及其他数据结构算法的复习

2.做分治或其他借助下标的题目要注意清楚绝对下标和相对下标。

BM48.数据流中的中位数

题目描述:

对动态数据流求中位数。如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

方法一:暴力方法

对于一组数据,我们可以用vector<int> arr来存取。如果对vector排好序,则很容易求出中位数。如果vector的大小为sz

  • 如果sz为奇数,假如为3,即[0 1 2],则中位数就是中间的那个数arr[1]
  • 如果sz为偶数,假如为4,即[0 1 2 3], 则中位数就是中间两个数的加权平均数。即 (arr[1] + arr[2]) / 2
class Solution {
public:
    #define SCD static_cast<double>
    vector<int> v;
    void Insert(int num)
    {
        v.push_back(num);

    }

    double GetMedian()
    { 
        sort(v.begin(), v.end());
        int sz = v.size();
        if (sz & 1) {
            return SCD(v[sz >> 1]);
        }
        else {
            return SCD(v[sz >> 1] + v[(sz - 1) >> 1]) / 2;
        }
    }

};

方法二:插入排序

对于方法一,可以发现有个优化的地方。
方法一中GetMEdian()操作,是每次都对整个vector调用排序操作。
但是其实每次都是在一个有序数组中插入一个数据。因此可以用插入排序。
所以:

  • Insert()操作可改为插入排序
  • GetMedian()操作可直接从有序数组中获取中位数
class Solution {
public:
    #define SCD static_cast<double>
    vector<int> v;
    void Insert(int num)
    {
        if (v.empty()) {
            v.push_back(num);
        }
        else {
            auto it = lower_bound(v.begin(), v.end(), num);
            v.insert(it, num);
        }
    }

    double GetMedian()
    { 
        int sz = v.size();
        if (sz & 1) {
            return SCD(v[sz >> 1]);
        }
        else {
            return SCD(v[sz >> 1] + v[(sz - 1) >> 1]) / 2;
        }
    }

};

方法三:堆排序

知识点:优先队列

优先队列即PriorityQueue,是一种内置的机遇堆排序的容器,分为大顶堆与小顶堆,大顶堆的堆顶为最大元素,其余更小的元素在堆下方,小顶堆与其刚好相反。且因为容器内部的次序基于堆排序,因此每次插入元素时间复杂度都是O(log2​n),而每次取出堆顶元素都是直接取出。

思路:

除了插入排序,我们换种思路,因为插入排序每次要遍历整个已经有的数组,很浪费时间,有没有什么可以找到插入位置时能够更方便。

我们来看看中位数的特征,它是数组中间个数字或者两个数字的均值,它是数组较小的一半元素中最大的一个,同时也是数组较大的一半元素中最小的一个。那我们只要每次维护最小的一半元素和最大的一半元素,并能快速得到它们的最大值和最小值,那不就可以了嘛。这时候就可以想到了堆排序的优先队列。

具体做法:

  • step 1:我们可以维护两个堆,分别是大顶堆min,用于存储较小的值,其中顶部最大;小顶堆max,用于存储较大的值,其中顶部最小,则中位数只会在两个堆的堆顶出现。
  • step 2:我们可以约定奇数个元素时取大顶堆的顶部值,偶数个元素时取两堆顶的平均值,则可以发现两个堆的数据长度要么是相等的,要么奇数时大顶堆会多一个。
  • step 3:每次输入的数据流先进入大顶堆排序,然后将小顶堆的最大值弹入大顶堆中,完成整个的排序。
  • step 4:但是因为大顶堆的数据不可能会比小顶堆少一个,因此需要再比较二者的长度,若是小顶堆长度小于大顶堆,需要从大顶堆中弹出最小值到大顶堆中进行平衡。
class Solution {
public:
    //大顶堆,元素数值较小
    priority_queue<int> min; 
    //小顶堆,元素数值都比大顶堆大
    priority_queue<int, vector<int>, greater<int>> max;
    //维护两个堆,取两个堆顶部即可
    void Insert(int num) {       
        //先加入较小部分 
        min.push(num);
        //将较小部分的最大值取出,送入到较大部分
        max.push(min.top());  
        min.pop();
        //平衡两个堆的数量
        if(min.size() < max.size()){  
            min.push(max.top());
            max.pop();
        }        
    }
    double GetMedian() {
        //奇数个
        if(min.size() > max.size())  
            return (double)min.top();
        else
            //偶数个
            return (double)(min.top() + max.top()) / 2; 
    }
};

 BM49.表达式求值

方法:栈 + 递归(推荐使用)

知识点:栈

栈是一种仅支持在表尾进行插入和删除操作的线性表,这一端被称为栈顶,另一端被称为栈底。元素入栈指的是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;元素出栈指的是从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

思路:

对于上述两个要求,我们要考虑的是两点,一是处理运算优先级的问题,二是处理括号的问题。

处理优先级问题,那必定是乘号有着优先运算的权利,加号减号先一边看,我们甚至可以把减号看成加一个数的相反数,则这里只有乘法和加法,那我们优先处理乘法,遇到乘法,把前一个数和后一个数乘起来,遇到加法就把这些数字都暂时存起来,最后乘法处理完了,就剩余加法,把之前存起来的数字都相加就好了。

处理括号的问题,我们可以将括号中的部分看成一个新的表达式,即一个子问题,因此可以将新的表达式递归地求解,得到一个数字,再运算:

  • 终止条件: 每次遇到左括号意味着进入括号子问题进行计算,那么遇到右括号代表这个递归结束。
  • 返回值: 将括号内部的计算结果值返回。
  • 本级任务: 遍历括号里面的字符,进行计算。

具体做法:

  • step 1:使用栈辅助处理优先级,默认符号为加号。
  • step 2:遍历字符串,遇到数字,则将连续的数字字符部分转化为int型数字。
  • step 3:遇到左括号,则将括号后的部分送入递归,处理子问题;遇到右括号代表已经到了这个子问题的结尾,结束继续遍历字符串,将子问题的加法部分相加为一个数字,返回。
  • step 4:当遇到符号的时候如果是+,得到的数字正常入栈,如果是-,则将其相反数入栈,如果是*,则将栈中内容弹出与后一个元素相乘再入栈。
  • step 5:最后将栈中剩余的所有元素,进行一次全部相加。
class Solution {
public:
    vector<int> function(string s, int index){
        stack<int> stack; 
        int num = 0;
        char op = '+';
        int i;
        for(i = index; i < s.length(); i++){
            //数字转换成int数字
            if(isdigit(s[i])){
                num = num * 10 + s[i] - '0';
                if(i != s.length() - 1)
                    continue;
            }
            //碰到'('时,把整个括号内的当成一个数字处理
            if(s[i] == '('){
                //递归处理括号
                vector<int> res = function(s, i + 1);
                num = res[0];
                i = res[1];
                if(i != s.length() - 1)
                    continue;
            }           
            switch(op){
            //加减号先入栈
            case '+': 
                stack.push(num);
                break;
            case '-':
                //相反数
                stack.push(-num);
                break;
            //优先计算乘号
            case '*':  
                int temp = stack.top();
                stack.pop();
                stack.push(temp * num);
                break;
            }
            num = 0;
            //右括号结束递归
            if(s[i] == ')')
                break; 
            else 
                op = s[i];
        }
        int sum = 0;
        //栈中元素相加
        while(!stack.empty()){  
            sum += stack.top();
            stack.pop();
        }
        return vector<int> {sum, i}; 
    }
    
    int solve(string s) {
        return function(s, 0)[0];
    }
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值