01归并排序
归并排序就是先将一个数组的左侧与右侧都有序,然后用两个指针分别比较两个数组元素的大小,将比较结果复制到辅助数组中。
整体采用递归方法,确定递归基是需要排序的序列只有一个数的时候。其余情况,则需要不断向下继续递归。
void mergesort(int l,int r,int arr[])
{
if(l == r)
return;
int m = l+(r-l)>>1;
mergesort(l,m,arr);
mergesort(m+1,r,arr);
merge(l,m,r,arr);
}
如果l=r的时候,说明需要排序的序列只有一个数,必然是有序的,所以直接返回。
接下来看merge方法,将两个有序序列合并成为一个有序序列的过程。
void merge(int l,int m,int r,int arr[])
{
int help[ARR_MAX];
int i = 0;
int p1 = l;
int p2 = m + 1;
while(p1<=m &&p2<=r)
{
help[i++] = (arr[p1]>arr[p2]? arr[p2++]:arr[p1++]);
}
while(p1<=m)
{
help[i++] = arr[p1++];
}
while(p2<=r)
{
help[i++] = arr[p2++] ;
}
for(i = 0; i < r-l+1; i++){
arr[l+i] = help[i];
}
}
- 开辟一个辅助数组,大小与原数组相同,该数组的元素用一个指针位置i来表示。
用p1代表左侧序列的下标位置,初始值就是左边界l,最大值是m,
p2代表右侧序列的下标位置,初始值就是m+1,最大值是r。 - 若两个序列的指针都没有超过最大值
若左侧序列的对应元素小于等于右侧序列的对应元素,就将左侧序列的对应元素复制到辅助数组中。若左侧对应数组大于右侧序列的对应元素,则将右侧序列的对应元素复制到辅助数组中。这一过程中,复制元素的序列和辅助数组,对应的指针位置都需要后移。 - 若有一个序列的指针越界,则需将另一个序列中剩余的元素都依次复制到辅助数组中。
- 最终将辅助数组中的元素,复制到原数组中。
完整测试代码
#include <iostream>
#include<time.h>
#define B_MAX 7
using namespace std;
void merge(int l, int m, int r, int* b)
{
int help[B_MAX];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r)
{
help[i++] = (b[p1] > b[p2] ? b[p2++] : b[p1++]);
}
while (p1 <= m)
{
help[i++] = b[p1++];
}
while (p2 <= r)
{
help[i++] = b[p2++];
}
for (i = 0; i < r - l + 1; i++) {
b[l + i] = help[i];
}
}
void mergesort(int l, int r, int* b)
{
if (l == r)
return;
int m = (r + l) / 2;
mergesort(l, m, b);
mergesort(m + 1, r, b);
merge(l, m, r, b);
}
int main()
{
srand((unsigned)time(NULL));
int b[B_MAX];
for (int i = 0; i < B_MAX; i++)
{
b[i] = rand() % 10;
cout << b[i] << ' ';
}
cout << endl;
mergesort(0, B_MAX - 1, b);
for (int i = 0; i < B_MAX; i++)
{
cout << b[i] << ' ';
}
system("pause");
return 0;
}
时间复杂度分析
使用master公式
T(N)=2T(N/2)+O(N)
a=2 b=2 d=1
符合log b a=d 所以归并排序的时间复杂度为O(N*logN)
02归并排序拓展
小和问题与逆序对问题
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子 1,3,4,2,5
1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,1,3;
2左边比2小的数,1;
5左边比5小的数,1,3,4,2;
所以小和是这些数累加起来和为16
分析:求小和就是遍历当前数组,选择一个数,让这个数与左边的数依次比较,若左边的数小于当前数,则将该数累加到小和结果中。换种情况来考虑问题,求小和就是看当前数的右侧有几个数比当前数大,那么就加几倍的当前数。换个角度分析之后,时间复杂度还是一样的,我们使用归并排序的思想,使得在两组序列merge的时候,计算当前的小和。因为这两个系列都是有序的,所以不需要全部遍历,只需要计算右侧序列的下标即可。
while (p1 <= m && p2 <= r) {//都不越界的时候
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
//只有左组比右组小,才产生小和数量增加的行为,当前右组有多少个数比p1大
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
在排序的同时记录小和结果,当右侧序列数大于当前左侧序列数的时候,小和增加(r - p2 + 1)倍的当前左侧数。当左组与右组的数相等时,需要先向下拷贝右组的数,因为不这样,不知道右组有几个数比当前数大。
完整代码
#include<iostream>
#define ARR_Length 5
using namespace std;
int merge(int l, int m, int r, int *b)
{
int help[ARR_Length];
int p1 = l;
int p2 = m + 1;
int i = 0;
int result = 0;
while (p1 <= m && p2 <= r)
{
result += b[p1] < b[p2] ? ((r - p2 + 1) * b[p1]) : 0;
help[i++] = b[p1] > b[p2] ? b[p2++] : b[p1++];
}
while (p1 <= m)
{
help[i++] = b[p1++];
}
while (p2 <= r)
{
help[i++] = b[p2++];
}
for (i = 0; i < r - l + 1; i++)
{
b[i + l] = help[i];
}
return result;
}
int mergesort(int l, int r, int *b)
{
if (l == r)
return 0;
int m = l + ((r - l) >> 1);
return mergesort(l, m, b) //左侧排好并取小和
+ mergesort(m + 1, r, b) //右侧排好并取小和
+ merge(l, m, r, b);//merge时产生小和
}
int main()
{
int b[ARR_Length] = { 1,3,4,2,5 };
int a = 0;
a = mergesort(0, ARR_Length - 1, b);
cout << a;
return 0;
}
逆序对
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有的逆序。
分析:找逆序的过程就是在归并的过程中,如果左边的数大于右边的数,那么计算左侧序列的下标,记录逆序数,然后将逆序数都打印出来
完整代码
#include<iostream>
#define B_Length 5
using namespace std;
int merge(int l, int m, int r, int b[])
{
int help[B_Length];
int p1 = l;
int p2 = m + 1;
int i = 0;
int result = 0;
while (p1 <= m && p2 <= r)
{
result += b[p1] > b[p2] ? (m - p1 + 1) : 0;
if (b[p1] > b[p2])
for (int i = p1; i <= m; i++) {
cout << b[i] << b[p2] << endl;
}
help[i++] = b[p1] > b[p2] ? b[p2++] : b[p1++];
}
while (p1 <= m)
{
help[i++] = b[p1++];
}
while (p2 <= r)
{
help[i++] = b[p2++];
}
for (i = 0; i < r - l + 1; i++)
{
b[i + l] = help[i];
}
return result;
}
int mergesort(int l, int r, int b[])
{
if (l == r)
return 0;
int m = l + ((r - l) >> 1);
return mergesort(l, m, b) + mergesort(m + 1, r, b) + merge(l, m, r, b);
}
int main()
{
int b[B_Length] = { 1,3,4 ,2,5 };
int a = 0;
a = mergesort(0, B_Length - 1, b);
cout << a;
system("pause");
return 0;
}
03堆
- 堆结构就是用数组实现的完全二叉树结构。
- 完全二叉树中如果每棵树的最大值在顶部就是大根堆
- 完全二叉树中如果每棵树的最小值在顶部就是小根堆
- 堆结构的heapinsert和heapify操作
- 堆结构的增大和减小
- 优先级队列结构就是堆结构
什么是完全二叉树?二叉树是满的,若是不满也是从左到右依次变满的
数组从0出发的连续一段可以对应成完全二叉树(数组元素按二叉树行依次添加)
堆结构节点与数组对应位置关系:
左孩子节点为2i+1,右孩子节点为2i+2 父节点为(i-1)/2
heapInsert:新节点插入进来,并向上调整形成大根堆的过程
分析:某个数现在处在index位置,往上继续移动,将当前数与父位置的数进行比较,若当前数大于父位置的数,则两个数交换位置,index更新到刚才父节点的位置。
//某个数现在处在index位置,往上继续移动
void heapinsert(int arr[],int index)
{
while(arr[index] > arr[(index-1)/2])//若当前的数大于父位置的数
{
swap(arr[index],arr[(index-1)/2]);
index = (index-1)/2;//换完后来到父位置,继续在循环中判断 直到不比父节点大 或到达根节点
}
}
使用for循环遍历数组,调用heapinsert
for(int i = 0;i < length;i++ )
{
heapinsert(arr,i);//用for循环传入要处理的index
}
时间复杂度分析
插入一个数需要比较数的高度次,也就是O(logN),N个数需要比较log1+log2+…+log(N-1)=O(N)
heapify 假设数组中某个值变小,重新将数组调整为大根堆的过程
分析:找到这个数的左右孩子的最大值,将左右孩子的最大值与这个数进行比较,
若孩子节点大于父节点,交换位置。
停止条件:左右孩子的最大值小于等于父节点或者 没有左右节点
//某个数在index位置,能否往下移动
void heapify(int arr[],int index,int heapsize)
{
int left = index * 2 + 1; //左孩子的下标
while(left < heapsize)
{ //两个孩子中,谁的值大,把下标给large
int largest = left + 1 < heapsize && arr[left]<arr[left + 1] ?
left + 1:left;
//父亲和较大孩子之间,谁的值大,就把下标给largest
largest = arr[largest] > arr[index] ? largest : index;
if(largest == index)//父节点就是三个节点中的最大值
break;
swap(arr[largest],arr[index]);
index = largest;//向下移动
left = index * 2 + 1;
}
}
04堆排序
- 先让整个数组都变成大根堆结构,建立堆的过程
1.1 从上到下的方法,时间复杂度O(N*logN)(0-0位置是大根堆,依次插入数组元素进行heapInsert)
1.2 从下到上的方法,时间复杂度为O(N)(先让最底层的节点heapify,再让上一层的,不断依次向上heapify) - 把堆的最大值与堆末尾的值交换,然后减少堆的大小后,再去重新调整堆为大根堆,一直周而复始,时间复杂度为O(N*logN)
- 堆的大小减小为0的时候,排序完成。
排序代码
void heapsort(int arr[],int heapsize)
{
while(heapsize > 0)
{
heapify(arr,0,heapsize);//O(logN) 0位置数往下heapify
swap(arr[0],arr[--heapsize]);//O(1) 0位置与当前末位置交换
}
}
堆排序完整代码
#include<iostream>
#include<time.h>
#define length 10
using namespace std;
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
void heapify(int arr[], int index, int heapsize)
{
int left = 2 * index + 1;
while (left < heapsize)
{
int largest = left + 1 < heapsize && arr[left + 1] > arr[left] ?
left + 1 : left;
largest = arr[index] < arr[largest] ? largest : index;
if (index == largest)
break;
swap(arr[index], arr[largest]);
index = largest;
left = 2 * index + 1;
}
}
void heapsort(int arr[], int heapsize)
{
while (heapsize > 0)
{
heapify(arr, 0, heapsize);//O(logN) 0位置数往下heapify
swap(arr[0], arr[--heapsize]);//O(1) 0位置与当前末位置交换
}
}
void heapinsert(int arr[], int index)
{
while (arr[index] > arr[(index - 1) / 2])
{
swap(arr[index], arr[(index - 1) / 2]);
index = (index - 1) / 2;
}
}
int main()
{
srand((unsigned)time(NULL));
int arr[length];
int heapsize = length;
cout << "arr = ";
for (int i = 0; i < heapsize; i++)
{
arr[i] = rand() % 10;
cout << arr[i] << " ";
heapinsert(arr, i);
}
cout << endl << "arr1 = ";
heapsort(arr, heapsize);
for (int i = 0; i < length; i++)
{
cout << arr[i] << " ";
}
cout << endl;
//system("pause");
return 0;
}
05堆排序扩展题目
已知一个几乎有序的数组,几乎有序是指如果数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
分析:将0-k位置的数置为小根堆,0位置必然是小根堆的最小值,将0位置弹出,将k+1位置的数插入,每次添加一个数就弹出一个数,没有数需要插入时,依次弹出小根堆中的数
排序java代码
public void sortedArrDistanceLessK(int[] arr, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();//默认为小根堆
int index = 0;
for (; index <= Math.min(arr.length, k); index++) {//把前k+1个数放到小根堆
heap.add(arr[index]);
}
int i = 0;
for (; index < arr.length; i++, index++) {//每次添加一个数,弹出一个数
heap.add(arr[index]);
arr[i] = heap.poll();
}
while (!heap.isEmpty()) {//没有数需要添加后,依次弹出小根堆中的数
arr[i++] = heap.poll();
}
}
06荷兰国旗问题
问题一
给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
分析:开辟一个小于等于num的区域,然后将这个区域的初始位置置于数组边界左侧,通过与数组元素的比较的过程,小于等于区域不断推着大于区域向右移动,直到将数组元素遍历完。
具体的比较规则
代码
#include<iostream>
#include<time.h>
#define arr_length 10
using namespace std;
//交换函数
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int arr[arr_length];
int cur = 0;
int num = 6;
int x = -1;
//生成随机长度为arr_length的数组,并输出
srand((unsigned)time(NULL));
cout<<"arr = ";
for(int i = 0;i < arr_length;i++)
{
arr[i] = rand()%10;
cout<<arr[i]<<" ";
}
cout<<endl;
//核心代码
while(cur < arr_length)
{
if(arr[cur] <= num)
{
swap(arr[cur++],arr[++x]);
}
else
{
cur++;
}
}
//输出交换后的arr1,arr2
cout<<"arr1 = ";
for(int i = 0;i<=x;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
cout<<"arr2 = ";
for(int i = x+1;i<arr_length;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
system("pause");
return 0;
}
问题二
给定一个数组arr,和一个数num,请将小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
分析:再看问题二,两个问题的区别就是问题二中需要将等于num的数放在中间,那么我们可以在问题一的基础上再开辟一个大于num的区域,位于数组的右侧。
小于区域推着等于区域奔向大于区域,大于区域往左压缩待定位置
代码
#include<iostream>
#include<time.h>
#define arr_length 5
using namespace std;
//交换函数
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int arr[arr_length];
int cur = 0;
int num = 6;
int x = -1;
int y = arr_length;
//生成随机长度为arr_length的数组,并输出
srand((unsigned)time(NULL));
cout<<"arr = ";
for(int i = 0;i < arr_length;i++)
{
arr[i] = rand()%10;
cout<<arr[i]<<" ";
}
cout<<endl;
//核心代码
while(cur < y)
{
if(arr[cur] < num)
{
swap(arr[cur++],arr[++x]);
}
else if(arr[cur] == num)
{
cur++;
}
else
{
swap(arr[cur],arr[--y]);
}
}
//输出交换后的arr1,arr2,arr3
cout<<"arr1 = ";
for(int i = 0;i<=x;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
cout<<"arr2 = ";
for(int i = x+1;i<y;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
cout<<"arr3 = ";
for(int i = y;i<arr_length;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
system("pause");
return 0;
}
07不改进的快速排序
快排1.0
基于问题一,整个数组中,拿最后一个数当做num
按规则划分区域后,将大于区域的第一个数与当前数(最后一个数 )交换位置
然后让左侧和右侧重复这个操作,每次递归都会有一个数排好位置
代码
#include<iostream>
#include<time.h>
#define length 10
using namespace std;
//交换函数
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
int partition(int arr[],int l,int r)
{
int num = arr[r]; //把最后一个数设为num进行比较
int x = l-1; //x为小于等于的区域,设为区域之前一个位置
int cur = l; //cur是当前遍历的指针
while(cur < r+1) //遍历所有位置
{
if(arr[cur]<=num)
{
swap(arr[cur++],arr[++x]);
}
else
cur++;
}
return x-1;
}
void quicksort(int arr[],int l,int r)
{
if(l<r)
{
int a = partition(arr,l,r);
quicksort(arr,l,a);
quicksort(arr,a+1,r);
}
}
int main()
{
//准备随机数组
int arr[length] ;
srand((unsigned)time(NULL));
cout<<"arr = ";
for(int i = 0;i < length;i++)
{
arr[i] = rand()%10;
cout<<arr[i]<<" ";
}
cout<<endl;
//意外情况直接返回(本题用不到)
if (arr == NULL || length < 2)
{
return 0 ;
}
//核心
quicksort(arr,0,length - 1);
//结果输出
cout<<"arr = ";
for(int i = 0;i<length;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
system("pause");
return 0 ;
}
快排2.0
分析快排1.0的缺点:每次只能找出一个等于num的数进行排序,如果存在多个相同的num还需要继续划分,多做了很多无用功。基于问题二荷兰国旗,每次可以搞定一批等于num的位置,会使时间复杂度的常数项大大降低。
代码
#include<iostream>
#include<time.h>
#define length 20
using namespace std;
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
int* partition(int arr[],int l,int r,int a[])
{
int x = l-1;
int y = r+1;
int cur = l;
int num = arr[r];
while(cur<y)
{
if(arr[cur]<num)
{
swap(arr[cur++],arr[++x]);
}
else if(arr[cur]>num)
{
swap(arr[cur],arr[--y]);
}
else
{
cur++;
}
}
a[0] = x;
a[1] = y;
return a;
}
void quicksort(int arr[],int l,int r)
{
int a[2] = {0};
if(l<r)
{
int *p = partition(arr,l,r,a);
quicksort(arr,l,*p);
quicksort(arr,*(p+1),r);
}
}
int main()
{
//生成随机数组
int arr[length];
srand((unsigned)time(NULL));
cout<<"arr = ";
for(int i = 0;i<length;i++)
{
arr[i] = rand()%10;
cout<<arr[i]<<" ";
}
cout<<endl;
quicksort(arr,0,length - 1);
//打印结果
cout<<"arr1 = ";
for(int i = 0;i<length;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
system("pause");
return 0;
}
08 随机快速排序
分析:荷兰国旗的快排2.0版本的缺点在于使用最后一个数作为num,那么此时的复杂度便于数据状况有关,若原数组的本来就是有序的,那么此时的复杂度便很高。如果从数组元素中随机选择一个数,就可以绕开原始数据状况,时间复杂度变为长期的期望值。
选择数据组随机一个数int num = arr[l+rand()%(r-l+1)];
代码
#include<iostream>
#include<time.h>
#define length 20
using namespace std;
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
//这是一个处理arr[l..r]的函数
//返回一个长度为2的数组
int* partition(int arr[],int l,int r,int a[])
{
int x = l-1;//返回数组的左边界
int y = r+1;//返回数组的右边界
int cur = l;
int num = arr[l+rand()%(r-l+1)];//等概率随机选一个位置,与最右侧的数交换
while(cur<y)
{
if(arr[cur]<num)
{
swap(arr[cur++],arr[++x]);
}
else if(arr[cur]>num)
{
swap(arr[cur],arr[--y]);
}
else
{
cur++;
}
}
a[0] = x;
a[1] = y;
return a;
}
void quicksort(int arr[],int l,int r)
{
int a[2] = {0};
if(l<r)
{
int *p = partition(arr,l,r,a);
quicksort(arr,l,*p);
quicksort(arr,*(p+1),r);
}
}
int main()
{
//生成随机数组
int arr[length];
srand((unsigned)time(NULL));
cout<<"arr = ";
for(int i = 0;i<length;i++)
{
arr[i] = rand()%100;
cout<<arr[i]<<" ";
}
cout<<endl;
quicksort(arr,0,length - 1);
//打印结果
cout<<"arr1 = ";
for(int i = 0;i<length;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
system("pause");
return 0;
}
随机选择一个数,与最后一个数交换,然后开始划分区域。最好情况与最坏的情况都是概率事件。每种事件都是1/n,概率与事件复杂度相乘累加求得期望得时间复杂度是O(N*logN)额外空间复杂度为O(logN)