归并排序,快速排序的原理——让难题迎刃而解

目录

1.递归

1.1 递归的运用

问题一:

思路讲解:

代码剖析:

1.2 递归时间复杂度的计算

2.归并原理

2.1 归并排序 

原理:

问题二:

代码实现: 

2.2 归并排序原理的运用

问题三 :

 问题四:

3.快速排序

1.1快排引入

问题五:

1.2 快排原理


 

1.递归

1.1 递归的运用

递归就不多介绍了,先递后归。

问题一:

输入一串数字,输出其最大的那个数

输入样例:

1 2 3 4 5 6

输出样例:

思路讲解:

 具体代码:

#include<iostream>
using namespace std;
#include<vector>
#include<cmath>
int max(int left, vector<int> v, int right)
{
	if (left == right)
		return v[left];
	int mid = left + ((right - left) >> 1);
	int leftmax = max(left, v, mid);
	int rightmax = max(mid+1, v, right);
	return fmax(leftmax,rightmax);
}
int main(){
	vector<int> v1;
	int num;
	while (cin >> num)
	{
		v1.push_back(num);
		if (cin.get() == '\n')
			break;
	}
	cout << max(0,v1,v1.size()-1)<< endl;
	system("pause");
	return 0;
}

代码剖析:

这题的代码其实很明了了,要提一下的是mid那句

int mid = left + ((right - left) >> 1);

 这句简单点写就是

int mid = left + ((right - left) /2);

为啥这么写呢?当然不是为了装x,如果直接写(left+right)/2,那么当数字的量过大时(left+right)会溢出,而>>1相对于/2要快一点。 

1.2 递归时间复杂度的计算

   看到刚刚的问题一,很多人会想:直接遍历不就完事了,干嘛这么麻烦。其实主要是为了

master公式求解递归的时间复杂度

master公式(子问题规模是等量的)

T(N) = a*T(N/b) + O(N^d) 

T(N)——为母问题的规模是N

T(N/b)——子问题规模都是N/b

a——子问题被调的次数

O(N^d) ——除子问题调用外,剩下的过程

来看看问第一写的函数:

max函数可以看成母问题,其子问题是等量的

	int leftmax = max(left, v, mid);

子问题一,规模N/2

	int rightmax = max(mid+1, v, right);

子问题二,规模N/2

子问题一共调用了两次

剩下的时间复杂度:判断和比较大小,O(1)

T(N) = 2*T(N/2) + O(1) 

情况一:如果前1/3中1/3后1/3调用递归,那么时间复杂度为

T(N) = 3*T(N/3) + O(1) 

情况二:如果前1/3与后2/3调用递归,那么不能用master公式

情况三:如果你在问题一的基础上还要把数全打印一遍,则时间复杂度为:

T(N) = 2*T(N/2) + O(N) 

单单知道这些还不足以求时间复杂度

 T(N) = a*T(N/b) + O(N^d) 

只要确定了a,b,d,就能求时间复杂度

  • log(b,a) > d :时间复杂度为O(N^log(b,a))
  • log(b,a) == d :时间复杂度为O(N^d * logN)
  • log(b,a) < d :时间复杂度为O(N^d) 

 所以问题一的时间复杂度为O(N),和遍历的时间复杂度一样。

2.归并原理

2.1 归并排序 

原理:

问题二:

在一组数的指定范围内排序

代码实现: 

#include<iostream>
using namespace std;
#include<vector>
#include<cmath>
void gb(vector<int> &v, int left, int mid, int right)
{
	int *arr = new int[right - left + 1];
	int p1 = left;
	int p2 = mid + 1;
	int i = 0;
	while(p1 <= mid && p2 <= right)
	arr[i++] = v[p1] <= v[p2] ? v[p1++] : v[p2++];
	while (p1 <= mid)
		arr[i++] = v[p1++];
	while (p2 <= right)
		arr[i++] = v[p2++];
	for (i = 0; i < right-left+1; ++i)
		v[left + i] = arr[i];
	delete [] arr;
}
void sort(int left, vector<int> &v, int right)
{
	if (left == right)
		return;
	int mid = left + ((right - left) >> 1);
	sort(left, v, mid);
	sort(mid+1, v, right);
	gb(v, left, mid, right);
}
int main(){
	vector<int> v1;
	int num;
	while (cin >> num)
	{
		v1.push_back(num);
		if (cin.get() == '\n')
			break;                        
	}
	int left, right;
	cin >> left;
	cin >> right;
	sort(left-1, v1, right-1);
	for (auto i : v1)
		cout << i<<' ';
	system("pause");
	return 0;
}

时间复杂度计算:

T(N)=2*T(N/2)+O(N)

(三个while循环利用的双指针将数放入数组,复杂度为O(N),for循环也是O(N))

a=2,b=2,d=1;

O(N*logN) 

空间复杂度O(N)

像选择排序,冒泡排序等,为什么时间复杂度是O(N)因为出现了许多重复比较:

排出第一个数——比较N次

排出第二个数——比较N-1次

......

N+N-1+N-2......+1

等差数列求和:N+N*(N-1)/2=a*N^2+b*N+c

时间复杂度O(N),当然代码方面一眼就能看出来。

2.2 归并排序原理的运用

问题三 :

小和问题

给定一个数组,返回每个左边比当前小的数累加起来的值

输入:

[1,3,4,2,5]

输出:

16

解释:

1左边没有,3左边1,4左边1,3,2左边1,5左边1,3,4,2

1+1+3+1+1+3+4+2=16

解题思路:

 为什么要排序呢?因为排好序可以直接用下标计算大于当前数的个数,从而省去遍历的时间。还有要注意的是,当左侧的数与右侧的数相等时,先拷贝右侧的数,应为只有这样才能算右侧比它大的有多少个。

 代码实现:

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
int gb(vector<int> &v, int left, int mid, int right)
{
	vector<int> arr(right - left + 1);
	int p1 = left;
	int p2 = mid + 1;
	int i = 0,res=0;
	while (p1 <= mid && p2 <= right)
	{
		res += v[p1] < v[p2] ? (right - p2 + 1) * v[p1] : 0;
		arr[i++] = v[p1] < v[p2] ? v[p1++] : v[p2++];
	}
	while (p1 <= mid)
		arr[i++] = v[p1++];
	while (p2 <= right)
		arr[i++] = v[p2++];
	for (i = 0; i < right-left+1; ++i)
		v[left + i] = arr[i];
	return res;
}
int sort(int left, vector<int> &v, int right)
{
	if (left == right)
		return 0;
	int mid = left + ((right - left) >> 1);
	return sort(left, v, mid) + sort(mid + 1, v, right) + gb(v, left, mid, right);
    //左最小和+右最小和+将合并的最小和
}
int main()
{
	int num = 0;
	vector<int> v1;
	while (cin >> num)
	{
		v1.push_back(num);
		if (cin.get() == '\n')
			break;
	}
	int res = sort(0, v1, v1.size() - 1);
	for (auto i : v1)
		cout << i << ' ';
	cout << endl;
	cout << res << endl;
	return 0;
}

 问题四:

逆序对

剑指 Offer 51. 数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5

 思路讲解:

这道题在leetcode上为困难难度,实际上暴力求解是可以实现的,但在leetcode上会超时,我们便可以用归并排序原理来解题(时间复杂度和空间复杂度依然有点高,只能说leetcode的大佬太多了),这题就不画图了,直接口述:

前面一个数字大于后面的数字可以看成在这个数上,只要后面有数小于前面就是一对逆序对,看有几个小就加几,那么我们的归并排序就需要逆序排列(如果左枝上的一个值,大于右枝上逆序的一个值,那么它一定大于这个值后面的值),且如果左枝和右枝相等时先打印右枝,这样就能避免遍历。

代码实现:

class Solution {
public:
int gb(vector<int> &v,int l,int mid,int r)
{
    vector<int> arr(r-l+1);
    int p1=l;
    int p2=mid+1;
    int i=0,count=0;
    while(p1<=mid && p2<=r)
    {
        count+=v[p1]>v[p2]?(r-p2+1):0;
        arr[i++]=v[p1]>v[p2]?v[p1++]:v[p2++];
    }
    while(p1<=mid)
    arr[i++]=v[p1++];
    while(p2<=r)
    arr[i++]=v[p2++];
    for(i=0;i<r-l+1;++i)
    v[l+i]=arr[i];
    return count;
}
int sort(vector<int> &v,int l,int r)
{
    int mid=l+((r-l)>>1);
    if(l==r)
    return 0;
    return sort(v,l,mid)+sort(v,mid+1,r)+gb(v,l,mid,r);
}
    int reversePairs(vector<int>& nums) {
        if(nums.size()<2)
        return 0;
     return sort(nums,0,nums.size()-1);
    }
};

3.快速排序

1.1快排引入

问题五:

给定一个数组arr与一个数num,把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求时间复杂度O(N),额外空间复杂度O(1)。

思路讲解:

既然无法开新数组进行遍历,那么方法如下:

代码实现:

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
void swap(int& a, int& b)
{
	a ^= b;
	b ^= a;
	a ^= b;
}
void cmp(vector<int>& v, int num)
{
	int p1 = 0, p2 = 0;
	while (p2 < v.size())
	{

		if (v[p2] <= num)
			swap(v[p1++], v[p2]);
		p2++;
	}
}
int main()
{
	int num = 0, num1;
	vector<int> v1;
	while (cin >> num)
	{
		v1.push_back(num);
		if (cin.get() == '\n')
			break;
	}
	cin >> num1;
	cmp(v1, num1);
	for (auto i : v1)
		cout << i << ' ';
	return 0;
}

拓展问题:

荷兰国旗问题:在上个问题基础上,把等于num的数放在中间

思路讲解:

将一个指针放前端,一个指针放末尾,再上图情况下不计算等于的情况,完成遍历即可,注意:后端交换后遍历的指针保持不动,因为交换后那个数没进行比较。

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
void swap(int& a, int& b)
{
	a ^= b;
	b ^= a;
	a ^= b;
}
void cmp(vector<int>& v,int num)
{
	int p1 = 0, p2 = 0, p3 = v.size()-1;
	while (p2<v.size())
	{
		if (p1<p2 && v[p2] < num)
			swap(v[p1++], v[p2++]);
		else if (p3>p2 &&v[p2] > num)
			swap(v[p3--], v[p2]);
		else
		p2++;
	}
}
int main()
{
	int num = 0,num1;
	vector<int> v1;
	while (cin >> num)
	{
		v1.push_back(num);
		if (cin.get() == '\n')
			break;
	}
	cin >> num1;
	cmp(v1,num1);
	for (auto i : v1)
		cout << i << ' ';
	return 0;
}

1.2 快排原理

在问题五的基础上,在num的左侧取num1,右侧的最右边取num2,在进行问题五,一直递归下去,排序完成,这便是快排1.0,而拓展问题的方法递归就是快排2.0,快排3.0是在2.0的基础上取num随机取。

综上所述:快排的时间复杂度完全靠运气,但经过数学期望的计算,其时间复杂度大约为O(N*logN) 

和归并排序一样

代码实现: 

#include <iostream>
#include <vector>
#include <ctime>
#define random(x)(rand()%x)
using namespace std;
void swap(int& a, int& b)
{
	int t = a;
	a = b;
	b = t;
}
vector<int> partition(vector<int>& arr, int L, int R) {
	//荷兰国旗问题,只是把num换成了最右侧的值
	int less = L - 1;
	int more = R;
	while (L < more) {
		if (arr[L] < arr[R]) {
			swap(arr[++less], arr[L++]);
		}
		else if (arr[L] > arr[R]) {
			swap(arr[--more], arr[L]);
		}
		else {
			L++;
		}
	}
	swap(arr[more], arr[R]);//此时的more是大于的最左侧位置,将它与最右侧用于比较的数交换,这个数顺利进入等于区间。
	vector<int> p(2);//p容器用于装等于区间最左和左右侧的下标。
	p[0] = less + 1; 
	p[1] = more;   //由于交换了,此时more位置上的数是比较的数,也是等号的最右侧
	return p;
}
void qsort(vector<int> &v,int l,int r)
{
	if (l < r) {
		swap(v[l + (int)(random(r - l + 1))],v[r]);//将随机找出的数与最右侧的数交换,相当于上面题目中的num
		vector<int> p = partition(v, l, r);//输出中间相等部分最左侧最右侧的下标
		qsort(v, l, p[0] - 1);//对等号区间的左右侧进行快排
		qsort(v, p[1] + 1, r);
	   }
}
int main()
{
	vector<int>v1;
	int num;
	while (cin >> num)
	{
		v1.push_back(num);
		if (cin.get() == '\n')
			break;
	}
	qsort(v1, 0, v1.size()-1);
	for (auto i : v1)
		cout << i << ' ';
	return 0;
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shany-Ming

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值