排序(插入排序、快排、归并、基数排序)

2017-06-07  本文已影响209人  凌云壮志几多愁

这篇文章是我在学习数据结构时作笔记的用途,这篇文章会纪录下我学习的几种排序算法,以及在学习的时候会加上配图说明!

相关的定义和基础

如果某组信息在内存空间中存储,则称为表(list)。如果存储于外存中则称为文件(file)。把信息中的对象称为记录,每个记录的信息划分为更小的单元,称为
  顺序查找对关键字域没有任何限制,而折半查找需要关键字有序的排列。其中折半查找可以使用二叉查找树来描述。

⚠️二叉查找树的特征
其左子树不大于根节点值,右子树不小于根节点值。而且各个子树还是一棵二叉查找树。

内部排序是指在表的规模足够小,能够全部放在内存中排序的方法。外部排序指数据规模太大,不能全部放在内存中时。这篇文章中我主要纪录的是内部排序算法,其中包含了:插入排序、快速排序、堆排序、归并排序、基数排序。

插入排序

插入排序类似于玩纸牌时,每次拿一张牌,将这张牌放在合适的位置,使手中所有纸牌按顺序排列。

void insertion_sort(ele list[], int n){
    /// 最坏情况时间复杂度:O(n*n)
    int i,j;
    ele next;
    for (i = 1; i < n; i++) {
        next = list[i];
        for (j = i - 1; j >= 0 && next.key < list[j].key; j--) {/// 说明不是按照升序排列
            list[j+1] = list[j];
        }
        list[j+1] = next;
    }
}

最坏情况的时间复杂度为:O(n*n),还可以通过检查输入表的相对无序性来估计插入排序的计算时间。为了计算相对无序性,需要测量每个记录是左无序(LOO)的区域长度。
如果为左无序记录的个数,则插入排序的计算时间为O((k+1)n)

⚠️插入排序适用范围:
当只有少数纪录是LOO纪录时,插入排序的运行效果很好。

快速排序

快速排序是平均时间性能非常好的排序方法。在学习快速排序时借鉴了阮一峰的解读坐在马桶上学算法排序算法动画演示

图解分析

测试一组数据:26,5,37,1,61,11,59,14,48,19,55;下图中提到的i,j在图中可能看得不清楚,所以我说明一下:i 用于纪录在一次移动过程中小于基准点数据的下标。j 则相反,它是纪录大于基准点数据的下标。


自此以第一个为基准点的元素移动完成,需要注意的是:先由i移动,当i移动停止时,移动j。在i和j的移动完成之后,需要i<j才能进行位置交换。

最后结果如下:


最后结果

编码实现

C语言实现
void quick_sort(int list[], int left, int right);
void quicksort(int list[], int n){
    quick_sort(list, 0, n - 1);
}

void swap(int *lhs,int *rhs){
    int temp = *lhs;
    *lhs = *rhs;
    *rhs = temp;
}
#define AfterMove 1
void quick_sort(int list[], int left, int right){
    
    if (left >= right) {
        return;
    }
    int pivot = list[left];/// 这里我就简单的取最左边为基准点
#if AfterMove
    /// 哨兵i,j
    int i = left + 1;
    int j = right;
    while (i < j) {
        /// 先移动i
        while (list[i] < pivot) {
            i++;
        }///找出当前不满足条件的元素所在的位置
        /// 再移动j
        while (list[j] > pivot) {
            j--;
        }
        if (i < j) {/// 进行元素位置互换
            swap((list + i), (list + j));
        }
    }
    if (*(list + left) > *(list + j)) {
        swap((list + left), (list + j));
    }
#else
    /// 哨兵i,j
    int i = left;
    int j = right + 1;
    do {
        /// 先移动i
        while (list[++i] < pivot) {}///找出当前不满足条件的元素所在的位置
        /// 再移动j
        while (list[--j] > pivot) {}
        if (i < j) {/// 进行元素位置互换
            swap((list + i), (list + j));
        }
    } while (i < j);
    /// 交换基准点和j位置元素
    swap((list + left), (list + j));
#endif
    /// 递归处理当前基准点左右两边元素
    quick_sort(list, left, j-1);/// 左边
    quick_sort(list, j+1, right);/// 右边
}

调用,以及最后的结果打印:

int list[11] = {26,5,37,1,61,11,59,14,48,19,55};
quicksort(list, 11);
for (int i = 0; i < 11; i++) {
  printf("Quick Sort Result is :%d\n",list[i]);
}
Quick Sort Result is :1
Quick Sort Result is :5
Quick Sort Result is :11
Quick Sort Result is :14
Quick Sort Result is :19
Quick Sort Result is :26
Quick Sort Result is :37
Quick Sort Result is :48
Quick Sort Result is :55
Quick Sort Result is :59
Quick Sort Result is :61
Swift实现

swift -v
Apple Swift version 3.1 (swiftlang-802.0.53 clang-802.0.42)

var list:Array<Int> =  [19,30,1,5,13,37,15,29,40,5]

func QuicSort(list:inout Array<Int>){
    __quick_sort(list: &list, left: 0, right: list.count - 1)
}

func __swap(_ lhs:inout Int,_ rhs:inout Int){
    let tmp = lhs
    lhs = rhs
    rhs = tmp
}

func __quick_sort(list:inout Array<Int> ,left:Int ,right:Int) -> Void{
    if left >= right {
        return
    }
    var pivot:Int = list[left]
    /**
    var i:Int = left + 1
    var j:Int = right
    while i < j {
        while list[i] < pivot {
            i = i + 1
        }
        while list[j] > pivot {
            j = j - 1
        }

        if i < j {
            __swap(&list[i], &list[j])
        }
    }
    if list[left] > list[j] {
        __swap(&list[left], &list[j])
    }
    */
    var i:Int = left
    var j:Int = right+1
    
    while i < j{
        repeat{
            i = i + 1
        }while list[i] < pivot
        
        repeat{
            j = j - 1
        }while list[j] > pivot
        
        if i < j {
            __swap(&list[i], &list[j])
        }
    }
    __swap(&list[left], &list[j])
 
    __quick_sort(list: &list, left: left, right:j - 1)/// 左边
    __quick_sort(list: &list, left: j + 1, right: right)/// 右边
}

QuicSort(list: &list)
print(list)/// [1, 5, 5, 13, 15, 19, 29, 30, 37, 40]

归并排序

归并排序:也是递归(迭代)、合并排序。主要是经过两步,将一组元素分解成多个子序列,然后使用递归或者迭代的方式合并成一个整体序列,在合并的过程对每个子序列进行排序。

将一组无序的序列分解成多个子序列

/// 将一个数组分割成length为1的n个子序列(n为原数组长度),第一步分割子序列
void __merge_sort(int list[], int n){
    int length = 1;
    int tmp_list[n];
    /// 做分割序列,并对每个子序列进行合并
    while (length < n) {
        __merge_pass(list, tmp_list, length, n);
        length *= 2;
        __merge_pass(tmp_list, list, length, n);
        length *= 2;
    }
}

将子序列合并

void __merge_pass(int list[], int tmp[], int length, int n){
    /// 将子序列两两进行merge
    int i;
    for (i = 0; i < n - 2*length; i += 2*length) {
        __merge(list, tmp, i, i + length, i + 2*length - 1);
    }
    
    if (i + length < n) {/// 说明当前这个子序列,后面还跟了一个子序列(<n)。所以需要merge最后两个子序列
        __merge(list, tmp, i, i + length, n - 1);
    }else{/// 当前只有一个子序列,直接放入即可
        for (int j = i; j < n; j++) {
            tmp[j] = list[j];
        }
    }
}

在合并子序列时候进行排序

/// ls := left_start
/// rs := right_start
/// re := right_end
void __merge(int list[], int tmp[],int ls,int rs,int re){
    int i,j;
    i = ls;
    j = rs;
    int k = i;/// tmp的下标标量
    while (i < rs && j <= re) {
        tmp[k++] = list[i] <= list[j] ? list[i++] : list[j++];
    }
    while (i < rs) {
        tmp[k++] = list[i++];
    }
    while (j <= re) {
        tmp[k++] = list[j++];
    }
}

调用

int merge_list[10] = {26,5,77,1,61,11,59,15,48,19};
__merge_sort(merge_list, 10);
for (int i = 0; i < 10; i++) {
  printf("merge_list Sort Result is :%d\n",merge_list[i]);
}

归并排序存在的最大问题就是需要一个额外的空间来进行排序!

堆排序

堆排序放在最大堆一节做说明!

基数排序

对于基数排序,其中有两个重要的概念,分别是:MSD(主位优先排序)、LSD(次位优先排序)。什么会有这两种不同的排序方式呢?因为基数排序主要是针对于待排序列纪录中存在多个关键字。针对这不同的关键字,导致我们在排序时可以有主位和次位之分!
  在基数排序中,把排序关键字用基数r分解为多个数位。当r=10时,就得到十进制的分解结果。当r=2时,就得到二进制的分解结果。
  下面通过一系列十进制数排序来说明一下基数排序。在排序的一系列数字中最大为3位数:
[179、208、306、93、859、984、55、9、271、33

271 -> 93 -> 33 -> 984 -> 55 -> 306 -> 208 -> 179 -> 859 -> 9
306 -> 208 -> 9 -> 33 -> 55 -> 859 -> 271 ->179 ->984 -> 93
9 -> 33 -> 55 -> 93 -> 179 -> 208 -> 271 -> 306 -> 859 -> 984

可以看出在前面三步完成之后,能够正确将原序列经过从小到大的顺序排序。

代码实现

#undef MAX_DIGIT
#define MAX_DIGIT 3

#undef RADIX_SIZE
#define RADIX_SIZE 10

typedef struct __list_node* list_node_ptr;
typedef struct __list_node{
    int key;
    list_node_ptr link;
}list_node;

/// 排序数字中最多由三位数字组成
list_node_ptr digit_sort(list_node_ptr ptr){
    if (!ptr) {
        return NULL;
    }
    list_node_ptr front[RADIX_SIZE],rear[RADIX_SIZE];
    ///1.基数排序,使用lsd排序,需要有三个基数:从个位数,十位数,百位数
    int digit,k;
    for (int i = 0; i < MAX_DIGIT; i++) {
        for (k = 0; k < RADIX_SIZE; k++) {
            front[k] = rear[k] = NULL;
        }
        ///2.依次放入上一次顺序排列的数列
        for (; ptr; ptr = ptr -> link) {
            digit = (ptr->key/(int)pow(10, i))%10;/// 拿出该数字中第个/十/百位上的数字
            if (!front[digit]) {
                front[digit] = ptr;/// 保证该数位上有数据时,front数组对应的链表元素指向第一个
            }else{
                rear[digit]->link = ptr;/// 这一步实际上是纪录ptr->link的
            }
            rear[digit] = ptr;///rear始终只想该链表的最后一个数据
        }
        ptr = NULL;
        ///3.每一轮按照基数放在对应的数组里面之后,需要将其取出,按照在front数组中的顺序链接ptr
        for (k = RADIX_SIZE - 1; k >= 0; i--) {
            /// 这里解释一下为什么要用倒叙的方式?为了让代码简单,我们从位数上数字最大的开始,依次向前来链接链表
            rear[k]->link = ptr;
            ptr = front[k];
        }
    }
    return ptr;
}

内部排序总结

排序算法 平均时间复杂度 最坏时间复杂度 额外空间复杂度 稳定性
插入排序 O(N*N) O(N*N) O(1) 稳定
快速排序 O(N*log(N)) O(N*N) O(log(N)) 不稳定
归并排序 O(N*log(N)) O(N*log(N)) O(N) 稳定
堆排序 O(N*log(N)) O(N*log(N)) O(1) 不稳定
基数排序 O(P(N+B)) O(P(N+B)) O(N+B) 稳定

说明:
1、由于快速排序是递归进行的,所以额外的空间复杂度是O(log(N))
2、归并排序由于需要一个额外的temp_list,故其空间复杂度为O(N)
3、由于快速排序、归并排序和堆排序的时间复杂度都是O(N*log(N)),但是由于快速排序的前面系数很小。平均时间性能,快速排序是最好的

具体的使用场景:

因此在实际使用中,把插入排序、快速排序和归并排序结合使用,即在归并排序中使用快速排序来处理子序列,而在快速排序中使用插入排序处理在其递归过程中的子序列。

上一篇下一篇

猜你喜欢

热点阅读