leetcode刷题笔记(4):排序

1、数组中的第K个最大元素(215)

《算法笔记》中介绍过查找第K个顺序统计量的O(n)复杂度算法:即利用快速排序的划分过程,每次可以随机确定一个第 i 大的元素,根据 i 和 k的关系在对应区间继续该过程,直到 i=k。为避免最坏情况下落入O(n^2)复杂度,使用随机选主元方法。

掌握快排的划分过程非常重要!!!

class Solution {
public:
    
    int divide(vector<int>&nums,int l, int r){
      if(l>=r) return l;
      //rand()%n产生的是[0,n-1]间的整数,这里要产生[l,r],所以加上l
      int i = l + rand()%(r-l+1);
      swap(nums[i],nums[l]);//随机选主元
      int st = l,ed = r, key = nums[l];
      while(st<ed){
        while(st<ed && nums[ed]<=key){
          //由于本题是找第K大的元素,所以把大于主元的放左边区间
          --ed;
        }
        nums[st] = nums[ed];
        while(st<ed && nums[st]>=key){
          ++st;
        }
        nums[ed] = nums[st];
      }
      nums[st] = key;
      return st;
    }

    int findKthLargest(vector<int>& nums, int k) {
      //srand(time(0))的意思是:用当前时间来设定rand函数所用的随机数产生演算法的种子值
      srand(time(0));
      k -=1;
      int len =nums.size()-1;
      int q= divide(nums,0,len);
      while(q!=k){
        if(k<q){
          q = divide(nums,0,q-1);
        }else{
          q = divide(nums,q+1,len);
          
        }
      }
      return nums[k];
    }
};

语法:随机函数的运用,srand(time(0))获得当前时间作为种子,rand()%n可以获得[0, n-1]间的随机整数

方法二:使用堆排序方法。

下面先练习堆的一些操作方法。(使用数组方式)

(1)建堆(以大根堆为例)

要求把原来的完全二叉树调整为堆,按完全二叉树的存储方法: i 结点的左孩子为在2i位,右孩子在2i+1位。时间复杂度为O(n)

建堆是从堆的底部开始枚举每个结点,对每个结点自上而下检查是否需要调整(downAdjust函数)。

//这里设heap是从1号位开始存储,共n个结点
//对数组在[l,r]范围内向下调整,其中l为待调整的结点
void downAdjust(vector<int>& heap, int l, int r){
  int i = l, j = i*2;//i为待调整结点,j为i结点的左孩子
  while(j <= r){//检查i是否有孩子
    //如果i还有右孩子,把j更新为最大的孩子结点
    if(j+1<=r && heap[j+1]>heap[j]){
      ++j;
    }
    //根据孩子和i的大小,决定是否需要调整
    if(heap[j] > heap[i]){
      swap(heap[j],heap[i]);
      //交换过后,要检查底下是否会受影响,也得调整,保持i为待调整结点,j为i结点的左孩子
      i = j;
      j = i*2;
    }else{//没有交换,无需再调整
      break;
    }
  }
}

//建堆
void createHeap(vector<int>& heap){
  int n = heap.size()-1;
  for(int i=n/2; i>=1; i--){
    //由于n/2之后的结点都是叶子结点,没有孩子,所以不需要调整
    downAdjust(heap,i,n);
  }
}

(2)删除堆顶元素

删除堆顶元素后,用最后一个元素覆盖堆顶,并进行向下调整,时间复杂度log(n)。

//删除堆顶元素
void deleteTop(vector<int>& heap){
  heap[1] = heap[n--];
  downAdjust(heap,1,n);
}

(3)插入元素

把元素放在最后,然后向上调整,时间复杂度log(n)。

//对数组在[l,r]范围内向上调整,其中r为待调整的结点,l一般设为1
void upAdjust(vector<int>& heap, int l, int r){
  int i = r, j = i/2; //i为待调整结点,j为i的父亲
  while(j>=l){
    if(heap[j] < heap[i]){
      swap(heap[i],heap[j]);
      i = j;
      j = i/2;
    }else{
      break;
    }
  }
}

void insert(int x){
  heap[++n] = x;
  upAdjust(heap,1,n);
}

(4)堆排序。

以递增排序为例,由于大顶堆堆顶存储着最大值,只要每次取出堆顶元素,逆序排列即可。为了节省空间,取出的堆顶放在末尾。这样时间复杂度O(nlgn),空间复杂度O(1)。

void heapSort(vector<int>& heap){
  createHeap(heap);//建堆
  for(int i=n ; i>1; i--){
    swap(heap[i],heap[1]);//倒着枚举,每次取出当前堆顶到后面
    downAdjust(heap,1,i-1);//删除堆顶,重新调整堆
  }
}

回到本问题,官方解答:建立大顶堆,删除k-1次堆顶后,这时的堆顶就是第k大的元素,注意数组范围是[0,n-1],与之前有所不同。

class Solution {
public:
    void downAdjust(vector<int>& nums,int l,int r){
      //由于序号从0开始,i结点的左孩子为2i+1,右孩子为2i+2
      int i = l, j = i*2+1;
      while(j<=r){
        if(j+1 <= r && nums[j+1] > nums[j]){
          ++j;
        }
        if(nums[i]<nums[j]){
          swap(nums[i],nums[j]);
          i = j;
          j = i*2+1;
        }else{
          break;
        }
      }
    }

    void createHeap(vector<int>& nums){
      int n = nums.size();
      for(int i=n/2;i>=0;i--){//逆序枚举
        downAdjust(nums,i,n-1);
      }
    }
   
    int findKthLargest(vector<int>& nums, int k) {
      createHeap(nums);
      int n = nums.size()-1;
      //删除k-1次顶点,第K大元素就在堆顶
      for(int i=n;i>n-k+1;i--){
        swap(nums[0],nums[i]);
        downAdjust(nums,0,i-1);
      }
      return nums[0];
    }
};

或者:对前k个元素建立小根堆,再将剩余元素逐个与堆顶相比,如果大于堆顶,则替换堆顶并调整。比较完后,小根堆里只剩下最大的k个元素,且堆顶就是第k大的元素。

class Solution {
public:
    void downAdjust(vector<int>& nums,int l,int r){
      //由于序号从0开始,i结点的左孩子为2i+1,右孩子为2i+2
      int i = l, j = i*2+1;
      while(j<=r){
        if(j+1 <= r && nums[j+1] < nums[j]){
          ++j;
        }
        if(nums[i]>nums[j]){
          swap(nums[i],nums[j]);
          i = j;
          j = i*2+1;
        }else{
          break;
        }
      }
    }

    //对前k个结点建立小根堆
    void createHeap(vector<int>& nums,int k){      
      for(int i=k-1;i>=0;i--){//逆序枚举
        downAdjust(nums,i,k-1);
      }
    }
   
    int findKthLargest(vector<int>& nums, int k) {
      createHeap(nums,k);
      int n = nums.size();
      //剩余元素逐个与堆顶比较,如果大于堆顶则替换
      for(int i=k;i<n;i++){
        if(nums[i]>nums[0]){
          nums[0]=nums[i];//替换原堆顶,重新调整
          downAdjust(nums,0,k-1);
        }
      }
      return nums[0];
    }
};

2、前 K 个高频元素(347)

思路:先遍历数组,用哈希方法存储每个数字出现次数(可以看作一种桶排序)。之后对这些桶按出现次数进行排序,把前k个元素返回即可。

复杂度分析:遍历进行映射O(n),之后用比较排序方法排序O(mlogm),m为不同数字个数。

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> cnt;//统计每个数字出现次数
        int max_count = 0;//统计最大次数
        for (const int& num : nums) {
            //遍历统计并更新max_count
            max_count = max(max_count, ++cnt[num]);
        }
        // unordered_map本身不支持sort排序,需要转化为pair放入vector中排序
        vector<pair<int, int>> temp;
        for (const auto& it : cnt) {
            temp.push_back(it); //map和unordered_map中元素都以pair形式存在
        }
        sort(temp.begin(), temp.end(), [](const pair<int, int>& a, const pair<int, int>& b) {
            return a.second > b.second;//按键值,即出现次数排序
            });

        vector<int> ans;
        for (const auto& it : temp) {
            ans.push_back(it.first);
            if (ans.size() == k) {
                break;
            }
        }
        return ans;
    }
};

语法:unordered_map无法用sort直接排序,但由于其内部元素是用pair形式,可以先转存为vector<pair<int,int>>形式,再进行排序。

这个排序过程也可以自己用桶排序思想试下,把出现次数相同的元素放在一起。但是实测效率不高,可能是测试用例中很多数字出现次数存在很大差别,用vector可能有些浪费,再用unordered_map可能会好些。

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
      unordered_map<int,int> cnt;//统计每个数字出现次数
      int max_count = 0;//统计最大次数
      for(const int& num: nums){
        //遍历统计并更新max_count
        max_count = max(max_count, ++cnt[num]);
      }

      //按出现次数进行桶排序
      vector<vector<int>> bucket(max_count+1);
      for(const auto& it : cnt){
        //把数字按出现次数放入对应桶内,即出现次数相同的放在一起
        bucket[it.second].push_back(it.first);
      }

      vector<int> ans;
      //倒着把bucket内的k个数字加入ans中
      for(int i = max_count;i>=0 && ans.size()<k;--i){
        for(const int& num : bucket[i]){
          ans.push_back(num);
          if(ans.size()==k) break;
        }
      }
      return ans;

    }
};

进一步思考这个问题,发现在遍历完成个数统计后,问题实质上变成了求一个无序数组的前k个大的元素。这与上一题求第K个统计量非常像,因此也可以用上面的快排思路或者是堆排序。

使用快排思路,也是看确定位次的idx和k的大小关系,如果[left, idx-1]长度大于k,则继续在左边找k个的划分;如果[left, idx]长度小于k,则结果为左边全部元素加上右边区间的前k-(idx - left +1)个元素。

class Solution {
public:
    void quickSort(vector<pair<int,int>>& arr,int left,int right,int k){
      int pivot = left + rand()%(right - left +1);
      swap(arr[left],arr[pivot]);
      //划分过程快捷写法,把大的划分到左边
      int idx = left,key = arr[left].second;
      for(int i=idx+1;i<=right;i++){
        if(arr[i].second>key){
          swap(arr[i],arr[idx+1]);
          ++idx;
        }
      }
      swap(arr[idx],arr[left]);

      if(idx-left >= k){//在左区间继续划分找出k个数
        quickSort(arr,left,idx-1,k);
      }
      else if(idx - left +1 <k){//左区间全部考虑,再加上右边的k-(idx-left+1)个元素
        quickSort(arr,idx+1,right,k-(idx-left+1));
      }
      
       
    }
    vector<int> topKFrequent(vector<int>& nums, int k) {
      unordered_map<int,int> cnt;//统计每个数字出现次数
      int max_count = 0;//统计最大次数
      for(const int& num: nums){
        //遍历统计并更新max_count
        max_count = max(max_count, ++cnt[num]);
      }
      // unordered_map本身不支持sort排序,需要转化为pair放入vector中排序
      vector<pair<int,int>> temp;
      for(const auto& it : cnt){
        temp.push_back(it); //map和unordered_map中元素都以pair形式存在
      }
      quickSort(temp,0,temp.size()-1,k);

      vector<int> ans;
      for(const auto& it : temp ){
        ans.push_back(it.first);
        if(ans.size()==k){
          break;
        }
      }
      return ans;
    }
};

使用堆思路。可以像上题那样,对前K个元素建小顶堆,再将剩余元素与堆顶比较,如果大于堆顶,则替换更新。最后,堆里就剩下前k大的元素。

这里,尝试练习用优先队列priority_queue(其底层由堆实现)实现这一思路。

如果当前堆元素小于k,说明堆还没建完,直接插入;否则要比较堆顶决定是否替换。

class Solution {
public:
    static bool cmp(pair<int,int>& a, pair<int,int>& b){
      //键值大的优先级高,放在队列前面,与sort的cmp排序结果正好相反
      return a.second > b.second;
    }
    vector<int> topKFrequent(vector<int>& nums, int k) {
      unordered_map<int,int> cnt;//统计每个数字出现次数
      int max_count = 0;//统计最大次数
      for(const int& num: nums){
        //遍历统计并更新max_count
        max_count = max(max_count, ++cnt[num]);
      }
      
      priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(&cmp)> q(cmp);
      
      for(auto& [num, count] : cnt){
        if(q.size()==k){
          if(q.top().second < count){
            q.pop();
            // 此函数用于将新元素插入优先级队列容器,该新元素将添加到优先级队列的顶部
            // 队列之后会按优先级要求自动调整
            q.emplace(num,count);
          }        
        }
         else{
            q.emplace(num,count);
          }
      }
      vector<int> ret;
        while (!q.empty()) {
            ret.emplace_back(q.top().first);
            q.pop();
        }
        return ret;
    }
};

语法:priority_queue的用法

定义方式:priority_queue<Type, Container, Functional>。其中,Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。

ex: priority_queue(int, vector<int>, less<int>> q;less<int>表示数字大的优先级高,放在队首。

代码中priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(&cmp)> q(cmp); 是自己给定比较方式cmp, decltype作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

注意优先队列比较函数定义的是优先级,优先队列默认优先级大的放在队首,这与sort的比较函数正好相反

优先队列的emplace(num)函数,会把num插入到队首,之后进行调整。

3、根据字符出现频率排序(451)

本题与上一题思路非常类似,先遍历统计字符出现次数,再按频次排序,最后按频次构造结果。

class Solution {
public:
    string frequencySort(string s) {
      //遍历统计各字符次数
      unordered_map<char,int> letter;
      for (char c : s){
        ++letter[c];
      }

      //转化为vector,按出现次数排序
      vector<pair<char,int>> word;
      for(const auto& it : letter){
        word.push_back(it);
      }
      sort(word.begin(),word.end(),[](const pair<char,int>& a,const pair<char,int>& b){
        return a.second > b.second;
      });


      //构造结果字符串
      string ans = "";
      for(const auto& it : word){
        int temp = it.second;
        while(temp!=0){
          ans += it.first;
          --temp;
        }
      }
      return ans;

    }
};

排序过程的sort也可以用桶排序代替。

class Solution {
public:
    string frequencySort(string s) {
      //遍历统计各字符次数
      unordered_map<char,int> letter;
      int max_count = 0;
      for (char c : s){
        max_count = max(max_count,++letter[c]);
      }

      //用桶存储对应频次的字母
      vector<string> bucket(max_count+1);
      for(const auto& it : letter){
        bucket[it.second].push_back(it.first);
      }
      /*也可以这么写
      for(const auto& [ch,num] : letter){
        bucket[num].push_back(ch); 
      }
      */
      string ans;
      for(int i = max_count; i>0; i --){
        for(const auto& ch: bucket[i]){
          for(int j = 0 ; j<i; j++){
            ans.push_back(ch);
          }
        }
      }
      return ans;


    }
};

4、颜色分类(75)

基本思路:先遍历统计数组中各个数字个数,再按照个数修改数组。共遍历2次数组。

class Solution {
public:
    void sortColors(vector<int>& nums) {
      int cnt[3] = {0};
      for(const int& num : nums){
        ++cnt[num];
      }

      int idx = 0;
      for(int i=0;i<3;i++){
        while(cnt[i]>0){
          nums[idx++] = i;
          --cnt[i];
        }
      }
        
    }
};

如何只用一次遍历呢?

方法一:双指针。排序的基本操作是交换元素,这里我们要把0和1交换到数组头部,可以设置两个指针P0和P1,分别交换0和1,其指示当前头部0和1的下一位。开始遍历数组,碰到0就换到P0位置,碰到1就换到P1位置。注意0要在1前面,所以有P0<=P1,但是P0交换时,可能会把0后面的1给交换出去,所以若P0<P1,说明0后面已经有1了,P0交换后,还得再换一遍P1。

class Solution {
public:
    void sortColors(vector<int>& nums) {
      int n = nums.size();
      int p0 =0, p1 =0;
      for(int i=0; i<n; i++){
        if(nums[i]==1){
          swap(nums[i],nums[p1]);
          ++p1;
        }
        else if(nums[i]==0){
          swap(nums[p0],nums[i]);
          if(p0<p1){//p0<p1,则p0位的1被换到i位置上了           
            swap(nums[p1],nums[i]);
          }
          ++p0;
          ++p1;
        }
      }
        
    }
};

类似的可以用P0和P2分别去交换0和2,不过P2从末尾向前移动。直到我们遍历位置超过P2。这里要注意的是:当nums[ i ] == 2时,会与P2交换,但是交换过后如果nums[ i ] ==0或2,还得继续进行交换。

class Solution {
public:
    void sortColors(vector<int>& nums) {
      int n = nums.size();
      int p0 =0, p2 =n-1;
      for(int i=0; i<=p2; i++){
        while(i<=p2 && nums[i]==2){
          swap(nums[i],nums[p2]);
          --p2;
        }
        if(nums[i] == 0){
          swap(nums[i],nums[p0]);
          ++p0;
        }
      }
     
        
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值