读书笔记-排序

Posted Mosthink

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读书笔记-排序相关的知识,希望对你有一定的参考价值。

【选择排序】

a[i++] —> a[n],从前往后看、选择最小值、一次交换到位

1,完整循环找到数组中最小的元素;

2,把这个最小的元素与a[0]交换;

3,在a[i]-an的子数组中重复1-2步骤;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for(int i = 0; i < n; i++) {

    int min = i;

    for(int j = i + 1; j < n; j++) {

        if(a[j] < min) 

            min = j;

    }

    swap(i, j, a);
}

简写:

1
2
3
4
5
for(int i =0; i < n; i++) {
    min = //本次循环的i...n中的最小的元素的index;

    //将min和本次循环的i两个元素交换;
}

特点:

选择排序的扫描路线:a[i++] —> a[n]

选择排序是在每个大循环下,通过完整子循环找到最小值后,退出子循环再进行交换,而不是一找到小于关系就交换,每次交换后,左侧的有序数组的位置是最终的;

运行时间和输入无关,扫描数组的次数是固定的,因为大循环和子循环的次数是固定的,前一次扫描并不为下一次扫描提供信息;

交换次数最少,因为是子循环完全结束后才进行一次交换,每次交换的结果都是最后的排序子结果,元素不用做二次挪动;

选择排序是一截一截往后看,将子数组中的最小元素交换到最前头的位置;

选择排序没有最好情况和最坏情况,扫描的次数是固定的,交换的次数已经是所有排序算法中最少的了;


【插入排序】

a[j—] —> a[0],从半路往前看、让元素尝试往前走不动为止

插入排序是一截一截往前看,将倒置的元素交换;对于一个元素,总是尝试往前走,走到不能走为止,因为前面的元素已然有序了,所以走不动的时候左侧元素也是刚刚重新有序;总是相邻元素交换,所以交换次数频繁,一次交换的位置未必是最终位置;

插入排序的扫描路线:a[j—] —> a[0]

1
2
3
4
5
6
for(int i = 1; i < n; i++) {

    for(int j = i; j > 0 && a[j] < a[j-1]; j--) 

        swap(j, j -1, a);
}

每次子循环的结果,左侧的元素肯定是在已知(已经扫描)的数组中有序的,所以每次子循环发生的条件是,如果a[j]小于a[j-1],则交换,然后继续进行j—之后的扫描;但如果a[j]不小于a[j-1],因为左侧在已知数组中是有序的,这个时候该子循环就不会发生了;

对已然有序的数组排序,插入排序是线性扫描,0次交换;

插入排序就是解决倒置的两个元素,交换的次数就是倒置的元素个数;

插入排序和选择排序都是平方级的运行时间,但插入排序通常比选择排序快一个常数,因为对于随机的数组来说,插入排序扫描的次数会由于数组中的部分有序而减少,但选择排序不会,必须全扫描;交换,则是插入排序比选择排序次数要多,选择排序交换次数最多不超过n;


【归并排序】

先2分排序子数组,再归并成原数组

原地归并算法:

1,先将所有元素赋值到一个新的数组中,此时数组是两截有序的数组;

2, 在原数组上讲新数组中的数本地归并回来:左边用尽取右边;右边用尽取左边;右边当前元素比左边小取右边;右边当前元素大于等于左边取左边;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void merge(Comparable[] a, int low, int mid, int hign) {

    int i = low, j = mid + 1;

    for(int k = low; k <= hign; k++) 

        aux[k] = a[k];

    for(int k = low; k <= hign; k++) 

        if(i > mid)                     a[k] = aux[j++];

        else if(j > hign)            a[k] = aux[i++];

        else if(aux[j] < aux[i])    a[k] = aux[j++);

        else                                  a[k] = aux[i++];
}

自顶向下的归并排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sort(a, 0, a.length - 1);

void sort(Comparable[] a, int low, int hign) {

    if(hign < = low)        return;

    int mid = low + (hign - low) / 2;

    sort(a, low, mid);

    sort(a, mid + 1, hign);

    merge(a, low, mid, hign);
}

归并算法的思路就是:两个单元素的数组是分别有序的,可以通过merge方法将其归并为一个2元素的有序数组,依此类推,两个a[low]到a[mid]和a[mid+1]到a[hign]的数组分别有序,可以通过merge方法将其归并为一个有序数组a[low]到a[hign];

归并自上而下,先分拆(sort)直至单元素数组,再归并(merge)回到原数组;

归并算法的几个优化策略:

1,对小规模数组改用插入排序,比如sort方法中发现hign - low <= 10,则用插入排序;

2, 测试数组是否已经有序,就是看a[mid]如果小于等于a[mid+1],就不用归并了;

归并排序的时间总是NlogN;

跟普通排序不需要额外空间不一样,归并排序是需要额外空间的,跟N成正比;

归并排序是某种程度上的空间换时间算法;

自底向上的归并:

1
2
3
4
5
6
7
8
9
10
void sort(Comparable[] a) {

    aux = new Comparable[a.length];

    for(int sz = 1; sz < a.length; sz = sz + sz) 

        for(int low = 0; low < N - sz; low += sz + sz) 

            merge(a, low, low + sz - 1, Math.min(low + sz + sz - 1, N -1);
}

自顶向下是 化整为零,自底向上是循序渐进;

归并排序用了aux辅助数组,是空间复杂度不是最优的;

任何比较排序算法的复杂度都不会低于lg(N!)~NlgN;


【快速排序】

每一次切分都使数组左右两边趋于有序

特点:

1, 快速排序是原地排序,只需要一个很小的辅助栈;归并排序无法做到;

2, 复杂度是NlgN;插入、选择等交换排序无法做到;

3, 快速排序的原理:将一个数组分成两个子数组,将两部分独立地排序。

快速排序和归并排序是互补的:

1, 归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;快速排序是当两个子数组都有序时整个数组也就自然有序了。

2, 归并排序中,递归调用发生在处理整个数组之前;快速排序中,递归调用发生在处理整个数组之后;

3, 归并排序是数组被等分为两半;快速排序中,切分的位置取决于数组的内容;

算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void sort(Comparable[] a) {

    random(a) //随机打乱数组,为了比避免每次的切分元素总是子数组中的最小元素

    sort(a, 0, a.length - 1);

}

void sort(Comparable[] a, int low, int hign) {

    if(hign <= low)

        return;

    int j = partition(a, low, hign);

    sort(a, low, j -1);

    sort(a, j + 1, hign);
}

递归调用切分实现排序的思路:

如果左子数组和右子数组都是有序的,那么由左子数组(有序且所有元素都小于等于切分元素+切分元素+右子数组(有序且所有元素大于等于切分元素)组成的结果数组也一定是有序的;

切分找j的条件

1, 对于某个j,a[j]已经排定;

2, a[low]到a[j - 1]中的所有元素都不大于a[j];

3, a[j+1]到a[hign]中的所有元素都不小于a[j];

注意,上面的2, 3说的都是j的左边和右边的元素分别不大于和不小于切分元素,但此时左右两边并不是有序的;

切分算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int partition(Comparable[] a, int low, int hign) {

    int i = low, j = hign + 1;

    Comparable v = a[0];

    while(true) {

        while(a[++i]  < v)

            if(i == hign)

                    break;

        while(a[--j] > v)

            if(j == low)

                break;

        if(i >= j)

            break;

        swap(a, i, j);

    }

    swap(a, low, j);

    return j;
}

两个小while分别做从左、从右往中间走的动作;

从左边走遇见的比切分元素大的元素,跟从右边走遇见的比切分元素小的元素,交换之;

因为一次大循环中用的都是a[lo],同一个参考值,那么通过这样的交换,左侧都小,右侧都大;

i, j相遇的最后一次交换,是a[j]被认为小小于切分元素,a[i]被认为大于切分元素,然后他们交换了位置,这一次是相邻交换,—j和—i使得i和j的索引也交换了,所以此时j就是上一次的i的位置,已经被小于切分元素a[lo]的上一次的a[j]给占用了;同时a[i]则是 大于a[lo]的元素;

所以,最后swap(lo, j),跟开始的切分元素=a[lo]相呼应;

切分的轨迹是这样的 :

lo….i….j…hi

lo….ij…….hi

lo….ji…….hi

j…..loi…….hi

归并排序是从底往上一层层合并有有序子数组;

快速排序是从上往下,循序渐进,一层层切分下去,每一次切分都使得数组呈两边大小合适状态,切到单元素数组的时候,整个数组基于n多个两边大小合适的小数组,而有序了;

切分元素不一定要选择a[low],随机选都行;

打乱数组顺序的意义:random(a) //随机打乱数组,为了比避免每次的切分元素总是子数组中的最小元素。

如果每次的切分元素总是最小的元素,那么每一次切分都只是分离成一个元素的数组和一个N-1长度的子数组,这使得数组会被切分N多次;

对于小数组,切换到插入排序能提高效率;

partition方法还可以用来找一个数组中【第k大的元素】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int lo = 0, hi = a.length - 1;

while(hi > lo) {

    int j = partition(a, lo, hi);

    if(j == k)

        return a[k];

    if(j > k)

        hi = j - 1;

    if(j < k)

        lo = j + 1;

    return a[k];

【堆排序】

先使堆有序,再一个个删除最大值,删除即把第一个最大元素往尾部交换以推出堆

上浮和下沉算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void swim(int k) {

    while(k > 1 && pq[k/2] < pq[k]) {

        swap(pq, k/2, k);

        k = k / 2;
}

void sink(int k) {

    while(2 * k <= N) {
        int j = 2 * k;

        if(j < N && pq[j] < pq[j+1]) //选取两个子节点中较大的一个往上交换

                j++;

        if(pq[k] >= pq[j])   //结束下沉,已经比字节点大了

                break;

        swap(pq, k, j); //下沉

        k = j;

}

public static void sort(Comparable[] a) {

    int N = a.length;

    //先使得堆有序

    for(int k = N/2; k >= 1; k--)  //从右到左扫,因为最终堆有序是右边总比左边小;

            sink(a, k, N);             //只扫描一半,因为一半之后的元素都是叶节点,就是为1的堆

    //再进行下沉排序,销毁堆有序

   while(N > 1) {      //把最大元素删除,然后放入堆缩小后数组中空出的位置
        swap(a, 1, N--); //a[1]就是最大元素,把其交换到最后一个,就是从堆有序堆中删除它

        sink(a, 1, N);  //被交换到a[1]的元素,通过在子堆中下沉,使得子堆再次有序

}                           //重复这个过程,最大元素总是往后一个一个走,最后整个数组就有序了

堆排序的复杂度:N*lgN,而且是原地排序,无额外空间消耗;


【SpaceToTime排序 空间换时间排序法】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int i = 0;

int max = array[0];

int len = array.length;

for(int i = 1; i < len; i++)  //找出最大值,做为空间数组的length

    if(array[i] > max)

        max = array[i];

int[] temp = new int[max + 1];

for(int i = 0; i < len; i++)

    temp[array[i]] = array[i];

int j = 0;

int max1 = max + 1;

for(i = 0; i < max1; i++) {

    if(temp[i] > 0]

        array[j++] = temp[i];
}

不计空间成本,把数组值映射到一个临时数组的下标,然后遍历临时数组,把大于0的数顺序放回原数组;

注:temp[i] = i;

array[i] > 0;

【Java.util.Arrays.sort】

对原始类型用三向切分的快速排序;

对引用类型用归并排序;


【Comparable Comparator】

一个类实现了Comparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。
Comparator可以看成一种算法的实现,将算法和数据分离,Comparator也可以在下面两种环境下使用:
1、类的设计师没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身
2、可以使用多种排序标准,比如升序、降序等

多键排序,用Comparator;

以上是关于读书笔记-排序的主要内容,如果未能解决你的问题,请参考以下文章

读书笔记——冒泡排序

《SQL必知必会》读书笔记上(第1~15章)

《算法图解》读书笔记 - 快速排序

《算法图解》读书笔记 - 快速排序

《算法导论》读书笔记

读书笔记 -- 算法导论(第二部分 排序和顺序统计学)