十大经典排序算法(打倒拖延症系列)

一个排序算法可视化的软件,很魔性。

链接:https://pan.baidu.com/s/1hCoMku7UL7IN4hIdYTsuJg  提取码:4y4v 


基本概念:


 稳定性的概念:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

稳定性的意义:举个例子,比如不同班级的同学,先按照分数排了顺序,再按照班级排序,这是稳定的排序就能保证按班排完序,分数还是原来有序的。

  1.冒泡排序 

 

void BubbleSort(int* arr, int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
            }
        }
    }
}
View Code

 

  2.选择排序

void selectSort(int *a, int length) {
    for (int i = 0; i < length; i ++) {
        int minIndex = i;
        for (int j = i + 1; j < length; j++) {
            minIndex = a[j] < a[minIndex] ? j : minIndex;
        }
        swap(a[i], a[minIndex]);
    }
}
View Code

  3.插入排序

插入排序在数组近乎于有序的时候,比O(nlogn)的排序是还要快。

void insertSort(int *a, int length) {
    for (int i = 1; i < length; i++) {
        int j, temp = a[i];
        for (j = i; temp < a[j-1] && j>0; j--) {
            a[j] = a[j-1];
        }
        a[j] = temp;
    }
}
View Code

  4.希尔排序(特殊插入排序)

间隔序列的取法有各种方案。最初shell提出取t/2向下取整,直到t=1。但由于直到最后一步,在奇数位置的元素才会与偶数位置的元素进行比较,这样使用这个序列的效率会很低。不同的序列会使希尔排序算法的性能有很大的差异 至今为止还没有一个最完美的增量序列公式,这里选用全为t/2向下取整

  希尔排序必须保证最后一次划分间隔为1,希尔排序又叫缩小增量排序

(这里盗用数据结构书上的图了~,可以说是很形象了,不过图中选取的增量序列是  t - 2)

void shellSort(int*data, unsigned int len)
{
    for (int div = len / 2; div >= 1; div = div / 2) {
        for (int i = 0; i < div; ++i) {
            for (int j = i + div; j < len; j += div) {
                int k, temp = data[j];
                for (k = j - div; k >= 0 && temp < data[k]; k -= div) {
                    data[k + div] = data[k];
                }
                data[k + div] = temp;
                /*for (k = j; k >= div && temp < data[k - div]; k -= div) {
                    data[k] = data[k - div];
                }
                data[k] = temp;*/
            }
        }
    }
}

  5. 归并排序

   该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序利用了完全二叉树特性,是一种十分高效的稳定排序。

  网上有原地归并排序的帖子,其实是错的,空间复杂度是降低到O(1)了,但是时间复杂度变成了O(n^2)。“归并排序 内部缓存法”,可以将归并排序空间复杂度降低到O(1)。

 

void merger(int *arr,int left, int mid, int right) {
    int *a = new int[right - left + 1];
    int i = left, j = mid + 1, cnt = 0;
    while (i <= mid&&j <= right) {
    //这里必须是≤,归并排序才是稳定的。 a[cnt
++] = arr[i] <= arr[j] ? arr[i++] : arr[j++]; } while (i <= mid)a[cnt++] = arr[i++]; while (j <= right)a[cnt++] = arr[j++]; i = 0; while (left <= right) { arr[left++] = a[i++]; } } void mergerSort(int *arr,int left,int right) { if (left >= right)return; int mid = (left + right) / 2; mergerSort(arr,left, mid); mergerSort(arr,mid + 1, right); merger(arr,left, mid, right); }

归并排序应用举例:

小和问题:

在随机元素,随机数组大小的数组中,找出左边比右边元素小的所有元素之和。

    例如:数组[4,2,5,1,7,3,6] 第一个元素4比2大,不算小和,5比4和2都大,那就是4+2=6;1比4和2和5都小,不算小和;7比前面的都大,那就是上次小和6+4+2+5+1=18;然后3前面比2和1大,那就是18+2+1=21;最后6比4、2、5、1、3都大,结果就是21+4+2+5+1+3=36。那么最后的结果就是36。

    public static int getSmallSum(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        return func(arr, 0, arr.length - 1);
    }

    public static int func(int[] s, int l, int r) {
        if (l == r) {
            return 0;
        }
        int mid = (l + r) / 2;
        return func(s, l, mid) + func(s, mid + 1, r) + merge(s, l, mid, r);
    }

    public static int merge(int[] s, int left, int mid, int right) {
        int[] h = new int[right - left + 1];
        int hi = 0;
        int i = left;
        int j = mid + 1;
        int smallSum = 0;
        while (i <= mid && j <= right) {
            if (s[i] <= s[j]) {
                smallSum += s[i] * (right - j + 1);
                h[hi++] = s[i++];
            } else {
                h[hi++] = s[j++];
            }
        }
        for (; (j < right + 1) || (i < mid + 1); j++, i++) {
            h[hi++] = i > mid ? s[j] : s[i];
        }
        for (int k = 0; k != h.length; k++) {
            s[left++] = h[k];
        }
        return smallSum;
    }
View Code

 逆序对问题

https://www.luogu.org/problemnew/show/P1908

#include <iostream>
#include <stdio.h>
using namespace std;

long long sum = 0;
int arr[500005];
int help[500005];

void merge(int left, int mid, int right) {
    int t = 0, i = left, j = mid + 1;
    while (i <= mid && j <= right) {
        if (arr[i] > arr[j]) {
            sum += (mid - i + 1);
            help[t++] = arr[j++];
        }
        else {
            help[t++] = arr[i++];
        }
    }
    while (i <= mid) help[t++] = arr[i++];
    while (j <= right)help[t++] = arr[j++];
    t = 0;
    while (left <= right) {
        arr[left++] = help[t++];
    }
}

void func(int l, int r) {
    if (l >= r) return;
    int mid = (l + r) / 2;
    func(l, mid);
    func(mid + 1, r);
    merge(l, mid, r);
}

int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        scanf("%d", &arr[i]);
    }

    func(0, n - 1);
    cout << sum << endl;
    return 0;
}
View Code

 

  6.快速排序

  快速排序的思想很简单,就是先确定一个基准 SIGN,从两边遍历数组将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

    快速排序选取第一个元素作为基准。这样在数组已经有序的情况下,算法将会降级为O(n^2)。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为基准。

    冯·诺依曼·赵四说过:“快速排序是看脸的,人丑排得慢。” 看来我是排不好了。

void partiton(int *a,int low,int high) {
    if (low >= high)return;
    //这里i要等于low,是为了j从hign一直到low都没找到比Sign小的值,就不需要进行交换
    int i = low, j = high;
    int sign = a[low];
    while (i < j) {
        //因为基准值是a[0],所以这里先j--,确保最后换的时候,a[j]是小于等于a[0]的。如果基准值是a[length-1],就是i++了。
        while (a[j] >= sign && i < j)j--;
        while (a[i] <= sign && i < j)i++;
        swap(a[i], a[j]);
    }
    swap(a[low], a[i]);
    partiton(a, low, i - 1);
    partiton(a, i + 1, high);
}

void quickSort(int *a, int length) {
    partiton(a, 0, length - 1);
}

 当数据元素完全有序(升序和降序)的情况下,会退化成O(n^2)的排序算法,因为递归的深度从理想情况下的logn变成了n层,随机选取主元可以一定程度解决这个为问题。

随机快速排序:

void partition(int *a, int left, int right) {
    if (left >= right) return;
    //获取left到right中的随机值
    int sign = rand() % (right - left) + left;
    swap(a[left], a[sign]);
    int i = left, j = right;
    while (i < j) {
        while (a[j] >= a[left] && i < j)j--;
        while (a[i] <= a[left] && i < j)i++;
        swap(a[i], a[j]);
    }
    swap(a[left], a[i]);
    partition(a, left, i - 1);
    partition(a, i + 1, right);
}
void RandomQuickSort(int *a,int length) {
    partition(a, 0, length - 1);
}
View Code

随机快速排序,时间复杂度的期望是nlogn,虽然快排和归并都是O(nlogn)的排序算法,但是merge过程和partition过程相比,归并排序输在了常数项上,而且还需要额外的空间,在实际运用中,快速排序用的更多。

还有种写法,每次partition将数组分成,小于 等于 大于,三个部分(讲道理,不好写)。

C++:主元选第一个

void partition(int a[], int left, int right) {
    //获取left到right中的随机值
    int sign = rand() % (right - left) + left;
    swap(a[left], a[sign]);
    int i = left, j = right;
    while (i < j) {
        if (a[j] > a[left]) {
            swap(a[j--],a[right--]);
        }
        else if (a[j] < a[left]) {
            //如果上面i初始值是left+1,这里应该是i++,终止条件是i<=j
            swap(a[j], a[++i]);
        }
        else {
            j--;
        }
    }
    swap(a[left], a[i]);
    //给出和 主元相等的值 的开始至结束下标
    p[0] = i; p[1] = right;
}

void RandomQuickSort(int a[],int left,int right) {
    if (left >= right) return;
    partition(a, left, right);
    RandomQuickSort(a, left, p[0] - 1);
    RandomQuickSort(a, p[1] + 1, right);
}
View Code

快速排序拓展延伸:

为什么说快排是不稳定的排序算法呢?

比如这种情况(主元选取第一个):

5 3 4 5 3 7

进行partition后第一个5就跑到第二个5后面去了。

寻找第k小(大)元素

int Quick_search(int left,int right,int k) {
    int i = left, j = right;
    int p = rand() % (right - left + 1) + left;
    int sign = a[p];
    swap(a[p], a[i]);
    while (i < j) {
        while (i < j&&a[j] >= sign)j--;
        while (i < j&&a[i] <= sign)i++;
        swap(a[i], a[j]);
    }
    swap(a[i], a[left]);
    if (i - left + 1 == k)return a[i];
    if (i - left + 1 < k)return Quick_search(i + 1, right, k - (i - left + 1));
    else return Quick_search(left, i - 1, k);
}

 

 7.基数排序

 时间复杂度:

分配的时间复杂度为O(n)
收集的的时间复杂度为O(radix)(就是选择的基数,一般是10)
分配和收集共需要distance趟(最大位数)
所以基数排序的时间复杂度为O(d(n+r))

  刚刚看到基数排序原理的时候,老夫脑子里只有一句话:“卧槽??”,很巧妙,而且很简单的思想。不难推出,基数排序适合于处理数据范围小的数据。

   

  基数排序属于很好懂,实现起来,很多小地方,比较繁琐。

  重要的是思想,为了缩短代码量,这里借助STL vector 实现了LSD(我怎么这么短系列):

void radixSort(int *a,int length) {
    //开19个,是为了处理负数,-9 --》9
    vector<vector<int>>radix(19);
    int max = a[0], count = 1, div = 1;
    //求最大值
    for (int i = 1; i < length; i++) 
        max = max > a[i] ? max : a[i];
    //求最大位数
    while (max /=10 ) count++;
    while(count--) {
        for (int i = 0; i < length; i++) {
                int index = a[i] < 0 ? -a[i] / div % 10 : a[i] / div % 10 + 9;
                radix[index].push_back(a[i]);
        }
        for (int i = 0, j = 0; i < length; j++ ) {
            int k = 0;
            while (k < radix[j].size()) a[i++] = radix[j][k++];
                radix[j].clear();
        }
        div *= 10;
    }
}

   这个是根据百科里的算法改的(百科里的代码竟然没考虑负数的???)

  因为把数据装桶的时候,是逆序装的,这里index直接用 a[j] / div % 10 + 9; 就行了

int MaxBit(int a[], int length) 
{
    int max = a[0], count = 1, div = 1;
    for (int i = 1; i < length; i++)
        max = max > a[i] ? max : a[i];
    while (max /= 10) count++;
    return count;
}
void radixsort1(int a[], int length) //基数排序
{
    int i,j,k,bit = MaxBit(a, length);
    int *tmp = new int[length];
    int *count = new int[19]; //计数器
    int div = 1;
    for (i = 1; i <= bit; i++){
        memset(count, 0, sizeof(int) * 19);
        for (j = 0; j < length; j++) {
            int index = a[j] / div % 10 + 9;
            count[index]++;
        }
            
        for (j = 1; j < 19; j++)
            count[j] += count[j - 1];
        //后面碰到的要往后放,tmp数组是倒过来的往里存的,这里必须逆序
        for (j = length - 1; j >= 0; j--) {
            int index = a[j] / div % 10 + 9;
            tmp[count[index]-- - 1] = a[j];
        }
        for (j = 0; j < length; j++)
            a[j] = tmp[j];
        div *= 10;
    }
    delete[]tmp;
    delete[]count;
}

   8.计数排序

  时间复杂度:Ο (n+k)(k是数据的范围)

  假定20个随机整数的值如下:

  9,3,5,4,9,1,2,7,8,1,3,6,5,3,4,0,10,9 ,7,9

遍历随机数列,每一个整数按照其值对号入座,对应数组下标的元素进行加1操作。

遍历完毕时,数组的状态如下:

直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次:

0,1,1,2,3,3,3,4,4,5,5,6,7,7,8,9,9,9,9,10

朴素版:

void countSort(int *a, int length) {
    int min = getMin(a, length);
    int max = getMax(a, length);
    int *count = new int[max - min + 1];
    memset(count, 0, sizeof(int)*(max - min + 1));
    for (int i = 0; i < length; i++) {
        count[a[i] - min]++;
    }
    for (int i = 0, j = 0; j < max - min + 1; j++) {
        if (count[j] != 0) {
            while (count[j]--) a[i++] = j + min;
        }
    }
}
View Code

  正版:

void countSort(int* a,int length) {
    int max = getMax(a,length);
    int min = getMin(a, length);
    int offset = max - min;
    int* countArray = new int[offset + 1];
    memset(countArray, 0, sizeof(int)*(offset + 1));
    for (int i = 0; i < length; i++) {
        countArray[a[i] - min]++;
    }
    for (int i = 1; i < offset + 1; i++) {
        countArray[i] += countArray[i-1];
    } 
    int* sortedArray = new int[length];
    for (int i = length - 1; i >= 0; i--) {
        //把a[i]放在该放的位置上
        sortedArray[countArray[a[i] - min] - 1] = a[i];
        countArray[a[i] - min]--;
    }
    for (int i = 0; i < length; i++)
        a[i] = sortedArray[i];
}

1.当数列最大最小值差距过大时,并不适用计数排序。

比如给定20个随机整数,范围在0到1亿之间,这时候如果使用计数排序,需要创建长度1亿的数组。不但严重浪费空间,而且时间复杂度也随之升高。

2.当数列元素不是整数,并不适用计数排序。

如果数列中的元素都是小数,比如25.213,或是0.00000001这样子,则无法创建对应的统计数组。这样显然无法进行计数排序。


 9.桶排序

上面说了,当数据范围较大、元素不是整数是不适合或不能使用计数排序,桶排序可以说是计数排序和基数排序的父亲,不属于比较排序,也不受O(n*logn)下限的影响。

更快的排序,总是以空间为代价的。

第一步:

首先确定有多少个桶,这里选取length个桶,每个桶的区间跨度为(max - min)/(length - 1)(具体建立多少个桶,如何确定桶的区间范围,有很多不同的方式。)

第二步:

遍历原始数列,把元素对号入座放入各个桶中:

int bucket(int x, int max, int min, int length) {
    //这里的length-1是除去特殊桶(装最大值的)的数目
    return (x - min) * (length - 1) / (max - min);
}
如果是,n个数,开 n + 1个桶,min 放在第0个桶,max 放在第n个桶,
就应该是:(x - min) * length / (max - min);
min, (min, min + d), [min + d, min + 2d), ……[max - d, max), max
这里length变大了1,实际意义就是除了min的位置不变,其余全部往后挪一个桶,只有第1个桶是(min,min+d),两侧都为闭区间。

第三步:

每个桶内部的元素分别排序,借助其他简单排序方法。

void bucketSort(int *a, int length) {
    int min = getMin(a, length);
    int max = getMax(a, length);
    vector<vector<int>>bucket(length);
    for (int i = 0; i < length; i++) {
        //length - 1 是最大桶的下标,这里等于省去了一个floor(a[i]-min)/((max-min)/(length-1))里的下取整
        int index = (a[i] - min)* (length - 1) / (max - min);
        bucket[index].push_back(a[i]);
    }
    for (int i = 0; i < length; i++) {
        //对每个桶 进行插入排序
        for (int j = 1; j < bucket[i].size(); j++) {
            int k, sign = bucket[i][j];
            for (k = j; k > 0 && sign < bucket[i][k - 1]; k--) {
                bucket[i][k] = bucket[i][k - 1];
            }
            bucket[i][k] = sign;
        }
    }
    for (int i = 0, k = 0; i < length; i++) {
        for (int j = 0; j < bucket[i].size(); j++) {
            a[k++] = bucket[i][j];
        }
        bucket[i].clear();
    }
}

 


 十、堆排序

  想实现堆排序,必须要先了解(就是优先级队列),堆排序就是,就是循环移除顶部元素到数组末尾,然后自上而下 heapify重建堆的操作。

堆排序可以分为两步:

1)根据初始数组去构造一个大根堆

从最后一个父节点开始往上,用 heapify(Sink) 调整最大堆,堆顶数组为最大元素。

2)每次交换第一个和最后一个元素,length--,然后把剩下元素重新调整为最大堆。 

这里需要知道几点:

数组下标是从0开始的,对于构建出来的二叉堆,第k个元素的左孩子就是2*k+1,右孩子是2*k+2

堆要求,所有子节点不大于堆顶,所以是可以处理存在重复数字的数组的。

void heapify(int *a, int index, int length) {
    while (index * 2 + 1 < length) {
        int left = index * 2 + 1;
        if (left + 1 < length&&a[left + 1] > a[left])left++;
        if (a[index] > a[left])break;
        swap(a[index], a[left]);
        index = left;
    }
}
void heapsort(int *a, int length) {
    for (int i = length / 2 - 1; i >= 0; i--) {
        heapify(a, i, length);
    }
    while (length--) {
        swap(a[0], a[length]);
        heapify(a, 0, length);
    }
}

小根堆也来一个:

void heapify(int *a,int index ,int length) {    
    while (index * 2 + 1 < length) {
        int left = index * 2 + 1;
        if (left + 1 < length&&a[left + 1] < a[left])left++;
        if (a[index] < a[left])break;
        swap(a[index], a[left]);
        index = left;
    }
}

for (int i = (length - 2) / 2; i >= 0; i--) {
        heapify(a, i, length);
    }
    for (int i = 0; i < 10; i++) {
        swap(a[0], a[length-1]);
        heapify(a,0,--length);
    }

 

posted @ 2019-01-29 14:11  czc1999  阅读(193)  评论(0编辑  收藏  举报