目录
1.递归
1.1 递归的运用
递归就不多介绍了,先递后归。
问题一:
输入一串数字,输出其最大的那个数
输入样例:
1 2 3 4 5 6
输出样例:
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;
}
问题四:
逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 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;
}