目录:
1|分治思想:快速排序,归并排序
2|小和,逆序对等问题求解
3|空间结构法之:堆排序,桶排序
4|排序算法小结
5|题解
1.分治法简介:问题划分为若干个子问题而与原来问题一致的子问题,递归解决这些问题,然后再合并这些结果,就得到了原问题的解
关键点:
1.原问题可以一直分解为形式相同子问题,当子问题较小时,可以自然求解,如一个元素本身有序.
2.子问题的解通过合并可以得到原问题的解
3.子问题的分解以及解的合并一定是简单的,否则分解和合并的时间复杂度甚至超过暴力解法,得不偿失.
归并是划分很简单,合并比较复杂
快排的思想重点在划分,合并不复杂
快排,重点是划分!:
1.分解:数组A[]被划分Wie两个子数组A[p,…q-1]和A[q+1,…r]使得A[q]为大小居中的数,左侧A[p,…q-1]的每个元素斗小于等于中间元素,而右侧元素A[q+1,…r]中的每个元素都大于等于中间元素,计算下标也算是划分的一部分.
2.解决:通过递归调用快速排序,分别对于划分的左侧和划分的右侧再次使用快排
3.和序:因为划分好后,每一个元素左边的元素都小于这个元素,右边的元素都大于这个元素,所以说,和序这部分根本就不用做,前两步都做好了.
Partition方法:
方法不唯一,但是我个人觉得看一两种最简单效率最高的方法就够了,剩下的了解一下就可以了,这一选择双向移动指针法.
法一:双向扫描法:头指针从左往右扫,找到大于主元的元素,再将右指针从右往左扫,找到小于主元的元素,两者交换,直到左侧无大于主元的元素,右侧无小于主元的元素.这里直接给出代码
#include<cstdio>
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
#include<math.h>
using namespace std;
int Partition(vector<int>& A, int begin, int end) { //问题的划分
int record = A[begin];
int i, j;
i = begin, j = end;
while (i<=j) {
while (A[i] <= record&&i<=j) {
++i;
}
while (A[j] >= record&&i<=j) {
--j;
}
if (i <= j) {
swap(A[i], A[j]);
}
}
swap(A[begin], A[j]);
return j;
}
void Quick_Sort(vector<int>& A, int begin, int end)
{
if (begin < end) {
int q = Partition(A, begin, end); //问题的分解与解决
Quick_Sort(A, begin, q - 1);
Quick_Sort(A, q + 1, end);
}
}
int main()
{
vector<int> S={ 1,2,34,543,4,234,15513,242,25343,43434 };
Quick_Sort(S, 0, S.size() - 1);
for (vector<int>::iterator a = S.begin(); a != S.end(); ++a) {
cout << *a << " ";
}
return 0;
}
这个划分思路上很是清晰,但是实际实现起来,调试起细节来足足花了我1个半小时(也就在学校才会这么认真doge),我们经过调试模拟最终情况后会发现,当情况为
<= <= >=时(此时刚刚走完一层内循环,也就是A[J]与A[I]的交换)
i 指向左边,j指向右边时,因为i的判断条件是小于等于j,所以i移动完后i=j的位置,这时,j因为所指元素>=记录元素record,所以且i=j(判断条件是i<=j),所以j向前移,也就说j+1=i,此时j所指向的元素小于record,交换A[begin]和A[j]即可
<= >= >=时,i同样是指向左边,j同样是指向了右边,因为i的判断条件是小于等于j,所以i再次移动后会指向中间的元素,j再次左移,会指向原来的i所指向的位置,所以此时的j又在i的左边并且紧邻着i,交换A[begin],A[j]即可.
上面这块比较抽象,大家最好手动模拟一下
快排优化
优化核心思想:每次取的主元(划分标准)最好可以把这个数组化分为两段大小相等的部分,如果每次都没选好,最后的时间复杂度退化为冒泡排序了.
方法一:三点(中值)优化法
方法二:绝对(中值)优化法
这里我只讲方法一:因为大部分库函数都是用三点优化法,对于方法二:绝对优化法虽然可以严格保证时间复杂度为O(nlogn)但是对于不是特别大量的测试数据,因为要先运用插入排序的思想求出数组的绝对中值而多花去O(n)的时间复杂度
int Partition(vector<int>& A, int begin, int end) { //问题的划分
int mid_Index;
int mid = begin + ((end - begin) >> 1);
if ((A[begin] >= A[mid] && A[begin] <= A[end])||(A[begin] >= A[end] && A[begin]<=A[mid]))
{
mid_Index = begin;
}
else if ((A[end] >= A[mid] && A[end] <= A[begin]) || (A[end] >= A[begin] && A[end] <= A[mid]))
{
mid_Index = end;
}
else {
mid_Index = mid;
}
swap(A[mid_Index], A[begin]);
这是前半段的,后面的就是Partition里面的内容,一个字符都没变的呦~,思路就是找出介于中间的值,这样有一个大致的参考,不确定性为%33,由原来的%100下降到%33,时间上肯定快了不少.
***归并排序:***分治模式
分解:将N个元素分成各含n/2个元素的子序列
解决:对两个子序列递归地排序
合并:合并两个已排序的子序列以得到排序结果
与快排的不同:
1.归并的划分比较随意
2.重点是合并
合并原理:两个数组,每次取它的第一个元素,相互比,较小的元素,实现合并是比较简单的
#include<cstdio>
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
#include<math.h>
#include<stdlib.h>
using namespace std;
vector<int> S = { 1,2,34,543,4,234,15513,242,25343,43434 };
void merge(vector<int>& A, int low, int mid, int high);
void merge_sort(vector<int>& A, int low, int high)
{
if (low < high) {
int mid = low + ((high - low) >> 1);
merge_sort(A, low, mid); //注意到快排的中间是不算进去的
merge_sort(A, mid + 1, high); //但是归并排序是算进去的
merge(A, low, mid, high);
}
}
vector<int>& helper(S);
void merge(vector<int>& A, int low, int mid, int high)
{//开辟辅助空间,把辅助空间作为队伍,把原数组作为目标空间,往回拷贝,只开辟一次空间就好,取它的最大元素个数
int k = 0; //计总数,填入原来位置
int i = 0;
int j = 0;
while (low+i != mid && mid + 1+j != high) { //左边没有走完,同时,右边也没有走完,则继续走
if (helper[low + i] >= helper[mid + 1 + j]) {
A[k++] = helper[mid + 1 + j];
j++;
}
else if (helper[low + i] < helper[mid + 1 + j]) {
A[k++] = helper[low + i];
i++;
}
}
if (low == mid) {
while (mid + 1 != high) {
A[k++] = helper[mid + 1 + j];
j++;
}
}
else if (mid + 1 == high) {
while (low != high) {
A[k++] = helper[low + i];
++i;
}
}
}
int main()
{
merge_sort(S, 0, S.size() - 1);
for (vector<int>::iterator a = S.begin(); a != S.end(); ++a) {
cout << *a << " ";
}
return 0;
}
算法题:五种常见方法
1.举例法,找出其中蕴藏的某种规律
2.模式匹配法(见多识广):看看现有问题和已知的基础算法之间有没有每种关系或者说变化
3.简化推广法:修改约束条件,比如数据类型或数据量,我们先来实现简化后的问题,求出解法后推出更加一般的算法
4.简单构造法:从n=1的结果入手,依次解决n=2,n=3…乃至到一般情况
5.数据结构头脑风暴:所以数据结构走一遭
算法案例:调整数组顺序使得奇数位于偶数前面:输入一个整数数组,调整数组中的数字顺序,使得所有奇数位于数组的前半部分,偶数位于后半部分,要求时间复杂度为O(n)
解法:双指针,交换
void f(vector<int>& A, int left, int right)
{
int i = left, j = right - 1;
while (i <= j) {
while ((A[i] & 1) != 0&&i<=j) { //判断奇数偶数
++i;
}
while ((A[j] & 1) == 0&&i<=j) {
--j;
}
swap(A[i], A[j]);
}
}
算法案例:超过一半的元素:数组中有一个数字出现的次数超过了数组长度的一般,找出这个数字
可以使用"第K个元素"里顺序统计的思想,求解第N/2下标的元素
int SelectK(vector<int>&A,int p,int r,int k)
{//此时传入的是A.size()/2
int q = Partition(A, p, k);
int qK = q - p + 1; //主元是第几个元素
if (qK == k) return A[q];
else if (qK > k) return SelectK(A, p, q - 1,k);
else {
return SelectK(A, q + 1, r, k-qK); //注意这里不是求第K个元素了,而是求第k-qK个元素
} //因为左边已经被舍弃了,需要从右边来找第k-qK个元素
}
最小可用ID:非负数组(乱序)中找出最小的可分配的id(从1开始编号),数据量为1000000
题意:最小的缺席的那个数
比如数组1342576910…(大于10),我们可以说最小可用id为8
法一:创建新的辅助空间,依次填入原数组数据,两次遍历数组,O(n)的时间复杂度,但是空间复杂度比较高(申请1000000个int空间,想想就觉得头大)
法二:不断的partition划分,每次划分就会缩小一半判断当前所指的元素array[index]与yindex的大小,如果大了,表示左边有空缺,如果相等,则表示右边有空缺,再递归调用函数即可,边界情况单独考虑.
第K个元素:以尽量高的效率求出一个乱序数组中按数值顺序排序的第k个元素值
核心思想:
int SelectK(vector<int>&A,int p,int r,int k)
{
int q = Partition(A, p, k);
int qK = q - p + 1; //主元是第几个元素
if (qK == k) return A[q];
else if (qK > k) return SelectK(A, p, q - 1,k);
else {
return SelectK(A, q + 1, r, k-qK); //注意这里不是求第K个元素了,而是求第k-qK个元素
} //因为左边已经被舍弃了,需要从右边来找第k-qK个元素
}
题目描述
寻找发帖水王:思想:消除法,相邻两个元素,以前面的元素为基准,计数count,相同则count++,不相同则减减,最后选择的candidates就是待选元素.
/当水王发帖数量大于一半时才可以这样写
void print(vector<int>& array)
{
int candidates = array[0];
int count = 1;
for (int i = 1; i < array.size(); ++i) //对于不相同的两个元素,直接消除,因为个数大于一半,所以剩下的元素candidates水王
{
if (count == 0) {
candidates = array[i];
count++;
}
if (array[i] == candidates) {
count++;
}
else {
count++;
}
}
count << candidates;
}
但是,当水王发的帖子数只有总数的一半时,我们会发现,消到最后时,总是最后一个或者倒数第二个(此时的倒数第二个就是candidates)为我们要寻找的水王,所以不失一般性,我们可以每次选array[i]时都和最后一个元素作比较,单独计数,当最后的计数为N/2时,就表示最后一个元素就是水王,否则,当前candidates就是水王
比如初始数组
aaaaa12345
两两消除,最后只剩下cnadidates=a;
最后一个元素为5,我们每次选取当前元素和最后一个元素比较,计数不足N/2,则可以确定当前candidates就是水王
再比如初始数组
5a2aa34a6a
两两消除,最后只剩下6和a,此时6是candidates,每个元素的比较使得a的count_last==N/2,也就确定最后一个元素就是水王
讲两道关于逆序对的题目:
1.合并有序数组:给定两个排序后的数组A,B,假设A后面有足够的空间容纳B,求合并后的数组
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
void merge(vector<int>& A, vector<int>& B)
{
int i = 0;
vector<int> C;
vector<int>::iterator a = A.begin();
vector<int>::iterator b = B.begin();
while( a != A.end() && b != B.end())
{
if ((*a) <= (*b)) {
C[i++] = *a;
a++;
}
else {
C[i++] = *b;
++b;
}
}
if (a != A.end()) {
while(a != A.end()) {
C[i++] = *a;
++a;
}
}
else if (b != B.end()) {
while (b != B.end()) {
C[i++] = *b;
++b;
}
}
for (int i = 0; i < C.size(); ++i) {
cout << C[i] << " ";
}
}
int main() {
vector<int> A = { 1,3,5,7,9 };
vector<int> B = { 2,4,6,8,10 };
merge(A, B);
return 0;
}
2.逆序对个数:一个数组,如果左边的数大,右边的数小,则称这两个数为一个逆序对,求数组中有多少个逆序对?
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
vector<int> C;
int nixv = 0;
void merge(vector<int>& A,int l,int mid,int r)
{
vector<int> helper(A);
int left = l, right = mid+1;
int current = l;
while (left <= mid && right <= r) {
if (A[left] <= A[right]) {
helper[current++] = A[left++];
}
else {
helper[current++] = A[right++];
nixv += mid - left + 1;
}
}
while (left <= mid) {
helper[current] = A[left];
current++;
left++;
}//从前到后再次合并为一个有序的数组
}
```cpp
扩充:如何生成一系列随机数范围为[a,b]?
#include<iostream>//里面有rand()函数,srand()种子,两者都没有返回值,但是rand()不接受参数,srand接收参数(因为要使得种子不同)
#include<time.h> //里面有time()函数,可以做强制类型转换
#define random(x) (rand()%x) //这里注意rand()函数是不接受参数的
int main()
{//只要种的种子不一样,得到的随机数也就不一样,
int a,b;
cin>>a>>b;
srand((int)time(0));
for(int i=0;i<10;++i)
{
cout<<random(a-b)+a<<" ";
}
return 0;
}
/*也就是两步走,1.种种子:srand(time(0)) 2.输出rand(a
-b)+a*/