排序
选择排序
void selectionSort(int arr[], int n){
int times=0;
while(times<n){// 循环n次,每次找第n小的元素
int minIdx=times;// 最小元素的索引
for(int i=times;i<n;i++){
if(arr[i]<arr[minIdx]){
minIdx=i;
}
}
swap(arr[minIdx],arr[times]);
times++;
}
}
插入排序
- 对于近乎有序的数组,排序效率很高,时间复杂度接近O(n)
// 基础版
void insertionSort(int arr[], int n) {
for(int i=0;i<n;i++){
for(int j=i;j>0;j--){
if(arr[j] < arr[j-1])
swap(arr[j],arr[j-1]);
else
break;
}
}
}
// 优化版
// 减少不断交换中的重复赋值
void insertionSort(int arr[], int n) {
for(int i=0;i<n;i++){
int temp=arr[i];// 记录待排元素
int j;// 记录待排元素应在的位置
for(j=i;j>0;j--){
if(temp < arr[j-1])
arr[j]=arr[j-1];
else
break;
}
arr[j]=temp;
}
}
归并排序
- O(nlogn):将全部元素分为logn层,每层的时间复杂度为O(n)
- 缺点是多使用O(n)个存储空间
- 自顶向下的归并排序
// 递归实现归并排序
void mergeSort(int arr[], int n){
__mergeSort(int arr[], 0, n-1);
}
// 自顶向下
// 基础版
void __mergeSort(int arr[], int l, int r){
if(l>=r)
return;
int mid=(l+r)/2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid+1, r);
__merge(arr, l, mid, r);
}
// 优化版
void __mergeSort(int arr[], int l, int r){
// 优化1:对于小规模数组, 使用插入排序
if(r-l<=15){
insertionSort(arr, l,r);
return;
}
int mid=(l+r)/2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid+1, r);
// 优化2:只有arr[mid]>arr[mid+1],才归并
// 对于近乎有序的数组非常有效
if(arr[mid]>arr[mid+1])
__merge(arr, l, mid, r);
}
// 将arr[l, mid]和arr[mid+1, r]两部分归并
void __merge(int arr[], int l, int mid, int r){
int *temp=new int[r-l+1];
for(int i=l;i<=r;i++)
temp[i-l]=arr[i];
int i=l, j=mid+1;
for(int k=l;k<=r;k++){
if(i>mid){
arr[k]=temp[j-l];
j++;
}
else if(j>r){
arr[k]=temp[i-l];
i++;
}
else if(temp[i-l]<temp[j-l]){
arr[k]=temp[i-l];
i++;
}
else{
arr[k]=temp[j-l];
j++;
}
}
delete[] temp;
}
- 自底向上的归并排序:可以通过索引直接获取元素,适合于对链表排序。同时也是一种非递归实现的归并排序
// 自底向上实现归并排序
void mergeSortBU(int arr[], int n){
// 第一轮循环:每次merge的元素个数(半区间)
for(int sz=1;sz <= n;sz+=sz){
// 第二轮循环:每次merge时起始的元素位置
for(int l=0;l+sz < n;l+=sz+sz){// 注意越界问题
// 对arr[l, l+sz-1]和arr[l+sz, l+sz+sz-1]两部分归并
__merge(arr, l, l+sz-1, min(l+sz+sz-1, n));
}
}
}
快速排序
- 基础版
-
缺点:对于近乎有序的数组,时间复杂度会退化为O(n^2),因为每次选最左边的元素作为标定点,导致分成的partition两部分数组长度差异较大,以此类推时间消耗巨大。
-
解决办法:随机选择数组中的元素作为标定点
void quickSort(T arr[], int n){
srand(time(NULL));// 初始化随机种子
__quickSort(arr, 0, n-1);
}
// 对arr[l,r]部分进行快速排序
void __quickSort(T arr, int l, int r){}
// 小规模数组用插入排序
if(r-l<=15){
insertionSort(arr, l, r);
return;
}
int mid=_partition(arr, l, r);
__quickSort(arr, l, mid-1);
__quickSort(arr, mid+1, r);
}
// 返回j,使得arr[l,j-1]<arr[j]; arr[j+1,r]>arr[j]
// 基础版
int _partition(T arr[], int l, int r) {
// 随机选择标定点:对于近乎有序的数组排序速度有较大提升
swap(arr[l], arr[rand()%(r-l+1)+l])
T target=arr[l];
// arr[l+1,j]<target; arr[j+1,i)>target
int j=l;
for(int i=l+1; i<=r; i++){
if(arr[i]<target)
swap(arr[i], arr[++j]);
}
swap(arr[l], arr[j]);
return j;
}
-
若数组中有大量重复元素,时间复杂度仍然会退化为O(n^2),因为大于标定点或小于标定点两部分中,有一部分会包含有大量重复元素,导致数组长度较长。
-
双路快排
int _parttion(T arr[], int l, int r){
// 随机选择标定点:对于近乎有序的数组排序速度有较大提升
swap(arr[l], arr[rand()%(r-l+1)+l]);
T target=arr[l];
// arr[l+1,i)<=target; arr(j,r]>=target
int i=l+1, j=r;
while(true){
while(i<=r && arr[i] < target) i++;
while(j>=l+1 && arr[j] > target) j--;
if(i>j) break;
swap(arr[i], arr[j]);
i++;
j--;
}
// i在第一个大于等于taget的位置,j在最后一个小于等于target的位置
swap(arr[l], arr[j]);
return j;
}
- 三路快排
void __quickSort3Ways(T arr[], int l, int r){
if(r-l+1)<=15{
insertionSort(arr, l, r);
return;
}
// partition
swap(arr[l], arr[rand()%(r-l+1)+l]);
T target=arr[l];
int lt=l;// arr[l, lt] < target
int gt=r+1;// arr[gt, r] > target
int i=l+1;// arr[lt+1, i) == target
while(i < gt){
if(arr[i]==target)
i++;
else if(arr[i]<target)
swap(arr[i++], arr[++lt]);
else// arr[i]>target
swap(arr[i], arr[--gt]);
}
swap(arr[l], arr[lt]);
__quickSort3Ways(arr, l, lt-1);// 注意这里是-1
__quickSort3Ways(arr, gt, r);
}
- 非递归实现的快速排序
void __quickSort(T arr[], int l, int r){
stack<int> s;
s.push(l);
s.push(r);
while(!s.empty()){
int r=s.top();
s.pop();
int l=s.top();
s.pop();
// 小数组用插入排序
if ((r - l + 1) < 15) {
insertionSort(arr, l, r);
continue;
}
int mid=_partion(arr, l, r);
if(mid-1 > l){// 左子序列
s.push(l);
s.push(mid-1);
}
if(mid+1 < r){// 右子序列
s.push(mid+1);
s.push(r);
}
}
}
排序算法衍生出的问题
- 求一个数组中逆序对的数量。数组中逆序对的数量可以衡量数组的有序程度。
// 暴力解法
int inversionCount(int arr[], int n){
int num=0;
for(int i=0; i<n-1; i++){
for(int j=i+1;j<n; j++){
if(arr[i] > arr[j])
num++;
}
}
return num;
}
// 归并排序法
long long __merge3(int arr[], int l, int mid, int r) {
int* aux = new int[r - l + 1];
long long count = 0;
int i = l, j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) {
aux[k-l] = arr[j];
j++;
continue;
}
else if (j > r) {
aux[k-l] = arr[i];
i++;
continue;
}
if(arr[i] > arr[j]){
count += (long long)(mid - i + 1);
aux[k-l] = arr[j];
j++;
}
else {
aux[k-l] = arr[i];
i++;
}
}
for (int i = 0; i < (r - l + 1); i++)
arr[l + i] = aux[i];
delete[] aux;
return count;
}
long long __inversionCount3(int arr[], int l, int r) {
long long count = 0;
for (int sz = 1; sz <= (r - 1 + 1); sz += sz)
for (int i = l; i + sz - 1 < r; i += sz + sz) {
count += __merge3(arr, i, i + sz - 1, min(i + sz + sz - 1, r));
}
return count;
}
// 对于一个大小为N的数组, 其最大的逆序数对个数为 N*(N-1)/2, 非常容易产生整型溢出
long long inversionCount3(int arr[], int n) {
return __inversionCount3(arr, 0, n - 1);
}
- 求一个数组中第n大的元素
int __partition(int arr[], int l, int r) {
swap(arr[l], arr[rand()%(r-l+1)+l]);
int target = arr[l];
int j = l;// arr[l+1, j]<=target
int i = j + 1;// arr[j+1, i)>target
for (; i <= r; i++) {
if (arr[i] > target)
continue;
else
swap(arr[i], arr[++j]);
}
swap(arr[l], arr[j]);
return j;
}
int __selectKth(int arr[], int l, int r, int k) {
int mid = __partition(arr, l, r);
if (mid < k)
__selectKth(arr, mid + 1, r, k);
else if (mid > k)
__selectKth(arr, l, mid - 1, k);
else
return arr[mid];
}
int selectKth(int arr[], int n, int k) {
srand(time(NULL));
return __selectKth(arr, 0, n - 1, k);
}
排序算法总结
- 快速排序的额外空间为O(logN):需要logN层的递归,这就需要logN个栈空间来保存每一层递归的临时变量以供递归返回时继续使用。
- 稳定排序:对于相等的元素,排序后,原来靠前的元素依然靠前。即像等元素的相对位置没有改变。
- 稳定排序与具体实现有关,可通过自定义比较函数,让排序算法不存在稳定性问题。