排序算法总结——插入、归并、快排

排序

选择排序

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++;
    }   
}

插入排序

  1. 对于近乎有序的数组,排序效率很高,时间复杂度接近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;
    }
}

归并排序

  1. O(nlogn):将全部元素分为logn层,每层的时间复杂度为O(n)
  2. 缺点是多使用O(n)个存储空间
  3. 自顶向下的归并排序
// 递归实现归并排序
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;
}
  1. 自底向上的归并排序:可以通过索引直接获取元素,适合于对链表排序。同时也是一种非递归实现的归并排序
// 自底向上实现归并排序
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));
        }
    }    
}

快速排序

  1. 基础版
  1. 缺点:对于近乎有序的数组,时间复杂度会退化为O(n^2),因为每次选最左边的元素作为标定点,导致分成的partition两部分数组长度差异较大,以此类推时间消耗巨大。

  2. 解决办法:随机选择数组中的元素作为标定点

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;
}
  1. 若数组中有大量重复元素,时间复杂度仍然会退化为O(n^2),因为大于标定点或小于标定点两部分中,有一部分会包含有大量重复元素,导致数组长度较长。

  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;
}
  1. 三路快排
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);
}
  1. 非递归实现的快速排序
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);
        } 
    }
}

排序算法衍生出的问题

  1. 求一个数组中逆序对的数量。数组中逆序对的数量可以衡量数组的有序程度。
// 暴力解法
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);
}
  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个栈空间来保存每一层递归的临时变量以供递归返回时继续使用。
  • 稳定排序:对于相等的元素,排序后,原来靠前的元素依然靠前。即像等元素的相对位置没有改变。
  • 稳定排序与具体实现有关,可通过自定义比较函数,让排序算法不存在稳定性问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值