快速排序
1.分而治之的思想:
快速排序采用分而治之的思想。
这个思想主要包括两个内容:
- 找出简单的基线条件。(即满足基线条件时,函数不再调用自己)
- 确定如何缩小问题规模,使其符合基线条件。
这里再解释一下基线条件的问题,可能看上面的看不太懂,打个比方。如果我们要将一个长方形均匀的分成尽可能大的正方形,应该怎么分?
是不是如果长刚好是宽的整数倍的时候就可以分了,如果不是整数倍,那我们就先在长上面划几个宽出来,剩下的继续找直到小长是小宽的整数倍为止。
所以上面这个问题的基线条件就是能找到长是宽的整数倍,就结束。
涉及数组递归时,大多数的基线条件都是”数组为空或者只包含一个元素”。
2.排序的基线条件
对于排序问题来说,最简单的数组是什么样?
就是不需要排序的数组,即空数组和只包含一个元素的数组。
void quicksort(array)
{
if (array.size()<2) return array;
}
3.快速排序的步骤:
- 基准值:我们找一个数作为基准值,放中间。
- 分区:找出比基准值小的值放左边,找出比基准值大的数放右边
- 如果此时子数组都是有序的,那么我们返回排好序的数组,左边+基准值+右边
- 如果不是,我们需要对子数组进行排序,对于只包含两个元素的数组或者空数组,都很好处理。
因此我们精简快排步骤是一个递归的过程:
- 选基准值
- 分区
- 对两个子数组进行快排
4.代码实现
标准库中函数qsort实现的就是快速排序,我们采用左闭右闭的写法。
当基准数选择最左边的数字时,那么就应该先从右边开始搜索;当基准数选择最右边的数字时,那么就应该先从左边开始搜索。不论是从小到大排序还是从大到小排序!
基本的思路就是:
一开始定第一个数为基准,然后j从右开始找一个比他小的数,i从左开始找一个比他大的数,然后交换两个数,一直到i j相等,此时他们所在的位置和第一个基准数交换位置,即完成了第一个数的定位,左边全部比他小,右边全部比他大。然后递归左边和右边两个子数组。
如果不能理解这个思路,可以参考下这篇图解博客
快速排序的时间复杂度:
O(nlog n)
void quick_sort(vector<int> &nums, int l, int r)
{
if (l >= r) return ;
int i = l, j = r;
int base=nums[l];//以第一个数为基准
while (i < j)
{
while (nums[j] >= base && i < j) //在右边找第一个比base小的
--j;
while (nums[i] <= base && i < j)//在左边找第一个比base大的
++i;
//二者交换
if (i != j)
{
nums[i] = nums[i] + nums[j];
nums[j] = nums[i] - nums[j];
nums[i] = nums[i] - nums[j];
}
}
//此时循环出来的左边比base小,右边比base大,需要交换base和中间数
nums[l] = nums[i];
nums[i] = base;
quick_sort(nums, l, i - 1);
quick_sort(nums, i + 1, r);
}
例:215数组中的第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
想法很简单,写一个快排,从大到小排,然后输出第k-1个数就行。
但是其实优化可以这样做,我们在进行快排的时候,不必快排完,如果跑到一半的时候i就已经和k-1相等了,那就直接返回,输出就完事儿了。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k)
{
quick_sort(nums,0,nums.size()-1,k);
return nums[k-1];
}
void quick_sort(vector<int>& nums,int l,int r,int k)
{
if(l>=r) return;
int i=l,j=r,base=nums[l];
while(i<j)
{
while(nums[j]<=base && i<j) --j; //从右边找第一个比base大的
while(nums[i]>=base && i<j) ++i;//从左边找第一个比base小的
if(i<j)//交换
{
nums[i]=nums[j]+nums[i];
nums[j]=nums[i]-nums[j];
nums[i]=nums[i]-nums[j];
}
}
//此时i,j左边的比base大,右边的比base小
//需要交换nums[i]和base
nums[l]=nums[i];
nums[i]=base;
if(i==(k-1)) return;
quick_sort(nums,l,i-1,k);
quick_sort(nums,i+1,r,k);
}
};
冒泡排序
这个比较简单,其实就是两重for循环,我们想象把一个数组竖过来,i从上往下走对于每一个i都有一个j从下往上比较相邻两数的大小,此以来达到每一轮i循环都能找到第i大的数。
冒泡排序有一个优化,就是如果当i跑到某一个位置,这时下面的数组已经是顺序的了,那我们的i就没有必要继续往下跑了,所以我们通常设置一个flag一开始为flase带进循环,如果j从下跑到i有进行交换,则置为true,否则为flase,当下一个i开始时发现是false就不跑了,直接跳出。
void bubble_sort(vector<int>&nums,int n)
{
bool flag=true;
for(int i=0;i<n && flag;++i)
{
flag=false;
for(int j=n-1;j>i;--j)
{
if(nums[j]>nums[j-1])
{
swap(nums[j],nums[j-1]);
flag=true;
}
}
}
}
比较简单,就不解释了。
时间复杂度:
O(n²)
归并排序
所谓归并,即把两个或两个以上的有序表组合成一个新的有序表。
归并排序主要两个步骤:
- 分割,每次从中间分开,直到每个子序列都只有一个(一个意味着必然有序,其实两个也行,没影响)
- 两两合并,保证合并之后的数组有序。需要两个指针从两个子序列头部开始跑,一个新的数组用于存放合并结果。指针所指位置进行比较,大的/小的(看需要)放入新数组,并后移被放入新数组的那个指针。
- 直到只有一个组
问题1:如何将两个有序数组合并?
void merge(int a[],int m,int b[],int n,int c[])//合并a[]和b[]到c[]中
{
int i,j,k; //三个指针
i=j=k=0;
while(i<m && j<n)//两个指针都没跑完的情况
{
if(a[i]<b[j]) //小的放前面,然后两个指针加一
c[k++]=a[i++];
else
c[k++]=b[j++];
}
//i先跑完了的情况
while(j<n) //把b里剩余的元素直接填入c中
{ c[k++]=b[j++]; }
//j先跑完的情况,同理
while(i<m)
{c[k++]=a[i++];}
}
所以,所谓归并,即先递归分割,再合并数列,即完成归并
代码:
void mergesort(vector<int> &nums,int l,int r,vector<int> &temp)//归并成有序数组,左闭右开
{
if(l+1>=r) return;//只剩一两个
int m=l+(r-l)/2;
mergesort(nums,l,m,temp);//[l,m-1]
mergesort(nums,m,r,temp);//[m,r] r为nums.size()
//下面步骤为合并两个有序数组为temp[l,r]
int i=l,j=m,k=l;
while(i<m || j<r)
{
if((i<m && nums[i]<=nums[j]) || j>=r)//升序,前者为正常情况,后者为j已经跑完了
temp[k++]=nums[i++];
else
temp[k++]=nums[j++]; //此种情况即为j<r&& nums[j]<=nums[i] || i>=m
}
//此时已经对temp[l,r]升序处理,把已经排好序的temp赋值给nums
for(i=l;i<r;++i)
{
nums[i]=temp[i];
}
}
桶排序
例347:前 K 个高频元素
顾名思义,桶排序的意思是为每个值设立一个桶,桶内记录这个值出现的次数(或其它属性),然后对桶进行排序。
针对样例来说,我们先通过桶排序得到三个桶 [1,2,3,4],它们的值分别为 [4,2,1,1],表示每个数字出现的次数。紧接着,我们对桶的频次进行排序,前 k 大个桶即是前 k 个频繁的数。
这里我们可以使用各种排序算法,甚至可以再进行一次桶排序,把每个旧桶根据频次放在不同的新桶内。
针对样例来说,因为目前最大的频次是 4,我们建立 [1,2,3,4] 四个新桶,它们分别放入的旧桶为 [[3,4],[2],[],[1]],表示不同数字出现的频率。最后,我们从后往前遍历,直到找到 k 个旧桶。
注: 这里注意k=2的时候我们从后往前找到的时候,第二个数是空的,也就是没有数字出现的频次是3,这个时候我们跳过了,找的下一个,所以写的时候注意会不会有频次是空的情况。
这里我们需要用到哈希表,用一个官方的容器unordered_map,下面简单介绍一下:
unordered_map:
unordered_map是c++11新加的一个标准容器,它提供了一对一的关系,并且在查找速度上能达到O(1)的速度。
1.构造函数:
unordered_map<string,int> mapstring;
unordered_map<int,string> mapint;
unordered_map<string,char> mapchar;
其中第一个参数为主值的类型,第二个参数为键值的类型。unordered_map内部使用哈希表进行存储与搜索。
2.添加数据
unordere_map<int,string> maplive;
//method 1
maplive.insert(pair<int,string>(102,"active"));
//method 2
maplive.insert(unordered_map<int,string>::value_type(321,"hello"));
//method 3
maplive[112]="April"; //这种方法是最简单用的最多的
3.查找
find()函数返回一个迭代器指向键值为key的元素,如果没有找到就返回指向map尾部的迭代器。
unordered_map<int,string>::iterator it;
//如果想要偷懒也可以这样写 auto it;
it = maplive.find(112);
if(it!=maplive.end())
cout<<"we find key 112.";
else
cout<<"we don't find 112.";
4.删除
erase(it)函数会删除迭代器it指向的元素。
//删除键值为112的元素
auto it;
it=maplive.find(112);
if(it==maplive.end())
cout<<"we don't find 112."<<endl;
else
maplive.erase(it); //delete 112
5.频次
count(a)会返回a元素在map中出现的次数,如果没有出现就返回0.
6.与map的区别
需要引入的头文件不同
map: #include < map >
unordered_map: #include < unordered_map >
map:内部实现了一个红黑树,该结构具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素,因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作,故红黑树的效率决定了map的效率。
小结
map的优点:
有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
红黑树,内部实现一个红黑书使得map的很多操作在的时间复杂度下就可以实现,因此效率非常的高
缺点:
空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点,孩子节点以及红/黑性质,使得每一个节点都占用大量的空间
适用处,对于那些有顺序要求的问题,用map会更高效一些
unordered_map: unordered_map内部实现了一个哈希表,因此其元素的排列顺序是杂乱的,无序的。
小结:
优点:
因为内部实现了哈希表,因此其查找速度非常的快
缺点:
哈希表的建立比较耗费时间
适用处,对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
unordered_map使用技巧:
初始化的时候最好统计一下别的信息,如每个key对应的value的最高频次max_mount,或者一共有多少个key值,方便使用桶排序时初始化。因为一般使用桶排序都是二维数组vector<vector< type>> buckets(num);你如果没有这个num的化,之后赋值比较麻烦,默认为空的vector访问buckets[n]容易造成越界。
回到例题
让我们回到例题,所以我们要先建立一个unordered_map,然后找出最高频次max_count,以此来建立max_count+1个桶(为什么要加一见注释):
unordered_map<int,int> mp;
int max_count=0;
//找到最大频次
for(auto i:nums)//对于nums里的每一个元素,对应到mp桶内自己位置+1
{
//eg:nums = [1,1,1,1,2,2,3,4],则mp[1]自加4次,mp[2]两次,mp[3]1次,mp[4]=1,但是mp从0开始,所以我们最高频次是4却接下来需要5个桶
max_count=(max_count,++mp[i]);
}
现在的情况是mp[1]=4,mp[2]=2,mp[3]=1,mp[4]=1;
我们如何把他排序呢,其实很简单,只需要把first和second对换位置即可。
我们创建一个二维的新桶(因为可能会有不同数字出现同一频次),
使得buckets[1]=[4],buckets[1]=[3] 即buckets[1]=[4,3]代表其只出现了一次的两个数。
buckets[2]=[2]
buckets[3]=0代表没有元素出现三次
buckets[4]=[1]
所以nums中的元素出现几次现在就在buckets里的第几行
vector<vector<int>> buckets[max_count+1];
for(auto j:mp)
{
buckets[mp.second].push_back(mp.first);
}
那么现在就可以从最后一行往前输出了:
vector<int> ans;//题目要求返回此类型
int flag=0;
for(i=buckets.end();i>=0 && flag!=k;--i)
{
for(auto:p:buckets[i])
{
ans.push_back(p);
++flag;
if(flag==k) break;
}
}
return ans;
总代码:
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k)
{
unordered_map<int,int> mp;
int max_count=0;
for(auto i:nums)
{
max_count=max(max_count,++mp[i]); //mp[1]=3,mp[2]=2,mp[3]=1;
}
vector<vector<int>> buckets(max_count+1);
for(auto j:mp)
{
buckets[j.second].push_back(j.first); //buckets=[[],[3],[2],[1]]
}
vector<int> ans;
int flag=0;
for(int p=max_count;p>=0 && flag!=k;--p)
{
for(auto q:buckets[p])
{
ans.push_back(q);
++flag;
if(flag==k) break;
}
}
return ans;
}
};
练习451. 根据字符出现频率排序
跟上面题目差不多
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
代码:
class Solution {
public:
string frequencySort(string s) {
unordered_map<char,int> mp;
int max_mount=0;
for(auto i:s)
{
++mp[i]; //mp[t]=1,mp[r]=1,mp[e]=2;
max_mount=max(max_mount,mp[i]);
}
//为了防止下表越界,我们在定义buckets的时候要给定大小
vector<vector<int>> buckets(max_mount+1);
for(auto j:mp)
{
buckets[j.second].push_back(j.first); //b[1]='tr' b[2]='e'
}
string str;
for(int p=max_mount;p>=0 &&str.size()<=s.size();--p )
{
for(auto q:buckets[p])
{
int flag=0;
while(flag<p)
{
str.push_back(q);
++flag;
}
}
}
return str;
}
};
75颜色分类
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
这个题目上来我的思路其实是统计一遍数字然后赋值。但是题目要求不能使用别的数组,要求原地排序。所以可能是考查对数组本身的操作。
解法一:快排
class Solution {
public:
void sortColors(vector<int>& nums)
{
quick_sort(nums,0,nums.size()-1);
}
void quick_sort(vector<int> &nums,int l ,int r)
{
if(l>=r) return;
int i=l,j=r;
int base=nums[l];
while(i<j)
{
while(i<j && nums[j]>=base) --j;
while(i<j && nums[i]<=base) ++i;
if(i!=j) swap(nums[i],nums[j]);
}
nums[l]=nums[i];
nums[i]=base;
quick_sort(nums,l,i-1);
quick_sort(nums,i+1,r);
}
};
这个题目其实非常简单,只需要从头到尾遍历一次,但凡找到0就放到最前面,找到1就不动,找到2就放到最后面。
但是遍历的时候肯定需要交换操作,这个时候双指针会方便一些。相向而行,相遇就结束.
设两个指针p指在头,q指在尾。然后指针i从头开始遍历,有以下情况:
- if nums[i]==0,这个时候我们应该将其放到数组前面,先与nums[p]交换,++p0;
- if nums[i]==2,放到后面,–q,但是如果此时换过来的nums[i]不是1而是0或者2,就要回退i(因为nums[i]为0、2的情况都有考虑,所以只要换回来的不是1,回退i就可以接着处理)
class Solution {
public:
void sortColors(vector<int>& nums)
{
int n=nums.size();
if(n==1) return;
int p=0,q=n-1;
for(int i=0;i<=q;++i)
{
if(nums[i]==0)
{
swap(nums[i],nums[p]);
++p;
}
else if(nums[i]==2)
{
swap(nums[i],nums[q]);
--q;
if(nums[i]!=1) --i;//因为nums[i]为0、2的情况都有考虑,所以只要换回来的不是1,回退i就可以接着处理
}
}
}