5.算法——排序算法,快速排序(分治思想,例215第K大元素),冒泡排序,归并排序,桶排序(例347前 K 个高频元素,unordered_map)练习451. 根据字符出现频率排序,75 颜色分类

快速排序

1.分而治之的思想:
快速排序采用分而治之的思想。
这个思想主要包括两个内容:

  1. 找出简单的基线条件。(即满足基线条件时,函数不再调用自己)
  2. 确定如何缩小问题规模,使其符合基线条件。

这里再解释一下基线条件的问题,可能看上面的看不太懂,打个比方。如果我们要将一个长方形均匀的分成尽可能大的正方形,应该怎么分?
是不是如果长刚好是宽的整数倍的时候就可以分了,如果不是整数倍,那我们就先在长上面划几个宽出来,剩下的继续找直到小长是小宽的整数倍为止。
所以上面这个问题的基线条件就是能找到长是宽的整数倍,就结束。

涉及数组递归时,大多数的基线条件都是”数组为空或者只包含一个元素”。

2.排序的基线条件
对于排序问题来说,最简单的数组是什么样?
就是不需要排序的数组,即空数组和只包含一个元素的数组。

void quicksort(array)
{
	if (array.size()<2) return array;
}

3.快速排序的步骤:

  1. 基准值:我们找一个数作为基准值,放中间。
  2. 分区:找出比基准值小的值放左边,找出比基准值大的数放右边
  3. 如果此时子数组都是有序的,那么我们返回排好序的数组,左边+基准值+右边
  4. 如果不是,我们需要对子数组进行排序,对于只包含两个元素的数组或者空数组,都很好处理。

因此我们精简快排步骤是一个递归的过程:

  1. 选基准值
  2. 分区
  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. 分割,每次从中间分开,直到每个子序列都只有一个(一个意味着必然有序,其实两个也行,没影响)
  2. 两两合并,保证合并之后的数组有序。需要两个指针从两个子序列头部开始跑,一个新的数组用于存放合并结果。指针所指位置进行比较,大的/小的(看需要)放入新数组,并后移被放入新数组的那个指针。
  3. 直到只有一个组

问题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就可以接着处理
            }
        }
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值