经典排序方法及细节小结(1)

在数据结构的学习中,排序是我们要重点学习的一部分;在掌握几种经典排序算法的同时,我们还要能够根据实际中遇到的情况,结合它们的时间复杂度、空间复杂度、稳定性这几个方面选择最合适的算法。现针对常用的几种经典排序算法和容易出错的细节总结一下:

1.插入排序

(1)直接插入排序

直接插入排序是一种最简单的排序方法,其基本思路是:

①将一条记录插入到已排好的有序表中,从而得到一个大小加1的有序表。

②每一步将一个待排序的数据元素,按其排序码的大小,插入到前面已经排好序的一组元素的合适位置上去

③循环①②步,直到元素全部插完为止。

假使按升序排序 当该序列为逆序时,每次调整都需将每个元素都需移到最前面,是最坏的情况,一共需移(n*(n-1))/2次;而当其序列基本有序,每个元素微调,一共就只需移动n次左右,便达到了最快的O(N)的复杂度。

时间复杂度:最好情况O(N)  最坏的情况O(N*N)  平均情况O(N*N)

容易看出若是相同的元素在排序前后,它们相对位置是没有变化的,所以

稳定性:稳定

代码如下:

void InsertSort(int* arr, size_t length)
{
    assert(arr);

    for(size_t i = 0; i< length -1; ++i)
    {
        int end = i;
        int tmp = arr[end + 1];
        while(end >= 0)
        {
            if(arr[end] <= tmp)
                break;
            else
            {
                arr[end+ 1] = arr[end];
                --end;
            }
        }
        arr[end + 1] = tmp;    

        //Print(arr, length);
    }

}

(2)希尔排序

希尔排序是直接插入排序的优化。优化的地方就在于它将待排序的序列分组进行插入排序,每组排好序后,整个序列就能达到基本有序;最后再进行一次直接插入排序即可。 如对下列序列排序:

具体思路:

①每隔gap大小个单位进行分组

②每个组内进行直接插入排序

③整体进行插入排序

注意:插入排序的快慢受序列有序程度影响,而希尔排序由于分组实现了插入排序使得整个序列希尔排序,整体提高了序列的有序度,提高了效率;同时,希尔排序再一个优化就是每次变化着选取gap来进行分组,这样每以gap分组排一次序后,就使序列更加有序,那么下一次再选一个gap分组排序就会更快了;同时选gap不妨每次除以3的方式来选,这样分得的组数就不会太多,或者太少(容易看出来,如果分得的组数太多,就和普通的插入排序没区别了;如果太少同样若如此)。

时间复杂度:最好情况:O(N)  最快情况:O(N*N)  平均情况:O(log(N^1.3))

从前面容易知道在直接插入排序中,相同元素相对的位置排完序后不会发生改变;而希尔排序在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以稳定性:

稳定性:不稳定

实现代码:

void ShellSort(int* arr, size_t length)
{
    assert(arr && length > 0);

    int gap = length;
    while(gap > 1) //gap ==1 时,继续往后走,进行最后一次整体的插入排序
    {
        //gap不能太大 ,太大使得分组太少,和普通插入排序没多大区别
        //同理也不能分得太多。
        gap = gap/3 + 1;  //加1防止gap为0

        //预排 将它们分成多个组,组内进行插入排序;使整体基本有序
        for(size_t i = 0; i < length - gap; ++i)
        {
            int tail = i;
            int tmp = arr[tail+ gap];
            while(tail >= 0)
            {
                if(arr[tail] <= tmp)   //tail位置处值不大于待插入的值,跳出
                    break;
                else
                {
                    arr[tail+ gap] = arr[tail];
                    tail-= gap;
                }

            }
            arr[tail + gap] = tmp;
        }
        printf("gap %d:", gap);
        Print(arr, length);
    }

}gap分趟排序结果:

2.交换排序

(1)冒泡排序

冒泡排序可能是我们最早接触的排序算法,尽管比较简单,但还是注意取有意义的变量名,同时还可以对其进行一点小的优化

思路:

①从第一个元素开始,相邻两元素比较大小,前面元素大(小),两者就交换位置,直到最大(小)的元素排到了最后;

②在剩下的N -1个元素中继续操作①,直到最后剩下一个元素,结束算法

优化:设置一个标志符flag,flag初始值是0,如果在一趟遍历中有出现元素交换的情况,那就把flag置为1。一趟冒泡结束后,检查flag的值。如果flag依旧是0,那这一趟就没有元素交换位置,说明数组已经有序,循环结束。

时间复杂度:最好情况:O(N)  最坏情况:O(N*N) 平均情况:O(N*N)

稳定性:稳定

代码:

void BubbleSort(int* a, size_t length) //冒泡排序
{
    assert(a);

    size_t end = length - 1;
    size_t i = 0;

    for (; end > 0; --end)
    {
        size_t flag = 0;
        for (i = 0; i < end; ++i)
        {
            if (a[i] > a[i + 1])
            {
                Swap(&a[i], &a[i + 1]);
                flag = 1;
            }
        }

        if (0 == flag)   //冒泡一趟,没有发生一次交换,序列有序
            break;
    }
}

(2)快速排序

基本思想就是递归分治,假设要排升序,选取一个标准key,将比key小的就全部放到其左边,比key大的就全部放到其右边;然后以排好后key值位置划分子区间,依次往后递归。

这里比较困难的地方就是实现每一次递归的单趟排序。总结下来,有以下3中方法来实现单趟排序(个人感觉难度依次往后增加)

(1)挖坑法

思路:

①假使把数组首元素值作基准key,此时数组第一个位置a[left]就相当于一个‘坑’。

②设置两个指针begin和end分别指向数组的首、尾元素。从end开始向左找到一个比key小的值,用它将begin处值覆盖,相当于填‘坑’

③再从begin处往右找一个比key大的值,用其覆盖end处的值,相当于再把右边 ‘坑’填上

④重复②③,begin=end时结束,最后将最后一个‘坑’填上。

如对{5,1,9,3,6,8,0,2,7,5}序列排序的单趟排序过程如图:

int PartSort1(int* arr, int left, int right)
{
    int tmp= arr[left];  //保存key值
    int begin = left;
    int end = right;
    while(begin < end)
    {
        while(begin < end && arr[end] >= tmp)  //找到比key小
            --end;
        arr[begin] = arr[end];

        while(begin < end && arr[begin] <= tmp) //找到比key大
            ++begin;
        arr[end] = arr[begin];
    }
    arr[begin] = tmp;   //填上最后留下的坑
    return begin;
}

(2)左右指针法

思路

①设置两个指针begin和end分别指向数组的首、尾元素,假使选第一个值作为基准值key。

②end依次往左查找比key小的值(先从end处往左找很关键!),begin依次往右查找比key大的值,都找到后就交换两者的值

③继续②至begin = end结束,最后将第一个位置和begin、end最后停下位置处的两个值交换。

注意:

(1)之所以要先从end处往左边找,是由选取的基准值key的位置决定的。如针对序列{6,8,0,2,7},选6为基准key,由于先从end往左找,最后begin、end会在小于key的位置停下 即{6,2,0,8,7} 中‘0‘的位置;而若先从左边往右找,最后则会停在比key大的位置上 即新序列{6,2,0,8,7}‘8’的位置,最后将6 和 8交换就会出现问题。

(2)基准值key是可以随机选择的。但要考虑到一个问题。随机选择的key值如果不是位置在两端的话 实现排序时到底是先从begin往右找 还是先从end往左找呢?以上面的序列进行分析:

①若选2为key

begin end最后停下位置在选的key的左边,因为先从begin往右找,两指针最后会在大于key的位置处停下,故先从begin往右找是不会出错。

②而序列改成{6,7,0,2,8}  此时若选‘7‘为key,

最后begin end停下位置在key的右边,因为先从end往左找,所以两指针最后会在比较码比key小的位置停下,先从end往右找正确。

所以先从哪边找,要看两指针最后停下的地方是在选定key的位置的左还是右。此处key的位置靠左,所以最后两指针很有可能在其右边停下;而当选定的基准key的位置在序列中间的某个地方时,最后两指针停在那?这就和黑洞样,不得而知了~(说起黑洞,今天的传来的消息 霍金老先生去了。世界很大,人在这世上,可能有些事情想一辈子也想不清楚)。额....跑远了   继续

细节处理:

比较细节的一步就是先将选的基准key换到最左边位置(或者最右边)确定了最后两指针停下的位置,这样就不会掉坑里了。

代码如下:

//左右指针交换法
int PartSort2(int* arr, int left, int right)
{
    //int key = arr[left];
    //三数取中法选取key值
    //int mid = left + ((right - left)>>1);
    //int key = GetMidNum(arr[left], arr[mid], arr[right]);

    //随机数法选key
    srand(time(NULL));
    int index = rand()%(right - left + 1);
    int key = arr[left + index];
    Swap(&arr[left], &arr[left + index]);  //细节问题

    int begin = left;
    int end = right;

    while(begin < end)
    {
        while(begin < end && arr[end] >= key)   //从右往左找 直到找到比key小的
            --end;
        while(begin < end && arr[begin] <= key)   //从左往右找 直到找到比key大的
            ++begin;
        Swap(&arr[begin] , &arr[end]);
    }
    //begin  end停下位置处值比key小
    Swap(&arr[begin], &arr[left]);
    return begin;
}

(3)前后指针法

思路:( 假使排升序)

选最右边的值作为基准key,设置两个指针cur prev,cur指向开头,prev指向前一个值。

cur prev依次朝一个方向走,每当cur向后找到比较码比key小的位置,prev找比较码不小于key的位置,交换prev cur处的值

当cur走到最右边时结束循环,交换最右边和prev+1位置处的值

注意:

1.  prev cur是同时往后面走。cur每走过一个比较码大于key的位置时就让prev停一次,当cur再走到一个关键码比key小的位置时,prev恰好在第一个比较码大于key的位置(相当于左边第一个待交换的位置)

2. 由于cur只有每走位置的比较码比key小时 prev才往后走,而将arr[right]值当作key,故prev一定最后在比较码不小于key的位置停下。因此才有步骤③

(这样这个过程不太好理解,建议将排序的数组定义成全局数组放置开头,在VS等调试器下仔细查看交换过程)

代码:

//前后指针法      //实现单链表的快排
int PartSort3(int* arr, int left , int right)
{
    int prev = left -1;
    int cur = left;
    int key = arr[right] ; //选取一个标准
    while(cur < right)
    {
        if(arr[cur] < key)  //cur对应值小于key prev紧随其后
        {
            ++prev;
            if(prev != cur)  //cur对应值>= key,prev不动 cur继续往后找比key小的位置
                Swap(&arr[cur], &arr[prev]);   //然后交换prev cur两个位置值
        }
        ++cur;
    }
    Swap(&arr[++prev], &arr[right]);
    return prev;
}

快排采用了分治思想,用递归里天然的函数栈帧来实现,于是自然就会想到它的非递归实现

快排非递归

用栈来保存每次分治的区间坐标

思路:

①先将最初始的区间坐标压入栈内

②从栈里获得排序序列的区间,再将划分的来到两个子区间压入栈内

③重复步骤②,直到区间长度为1结束循环

代码:

void QuickSortNonR(int* arr, int left, int right)
{
    assert(arr);
    if(left >= right)
        return;

    Stack s;
    StackInit(&s);
    StackPush(&s, left);
    StackPush(&s, right);
    while(StackEmpty(&s) != 0)  //
    {
        int end = StackTop(&s);
        StackPop(&s);
        int begin = StackTop(&s);
        StackPop(&s);

        int div = PartSort3(arr, begin, end);
        if(begin < div -1)   /左/区间长度大于1
        {
            StackPush(&s, begin);
            StackPush(&s, div -1);
        }
        if(end > div +1)  //右区间长度大于1
        {
            StackPush(&s, div+ 1);
            StackPush(&s, end);
        }
    }
}

快排的优化

①随机数法:选key时是有可能每次都选到区间的边界值,比如对一个降序序列进行升序排序,每次选得的key就十分尴尬了。随机数法每次随机的选择一个位置的值作key值,这从概率上来讲会提高排序的效率就不言而喻了。

但是即便如此,还是有可能会选到区间两端的值作key,于是又有了下面一种选key的方法

②取中位数法:将区间两端的值和中间位置处的值进行比较,取大小不大不小的数(中位数)作key. 这样进一步提高了效率

③小区间法:切割区间时,当区间内元素数量比较少时就不用切割区间了(递归太深影响效率),这时候就直接对这个区间采用直接插入法,可以进一步提高算法效率。

如果每次选取的key值大小都恰好是最中间的值时,把划分的区间连接起来就像一颗二叉树一样,在此种情况每确定一个key值的位置只需longN的时间即可。这样便可看出其时间复杂度了,同时当快排经过多次优化后近似看作最好的情况。所以

时间复杂度:最好情况:O(NlogN)     最坏:O(N*N)   平均: O(NlogN)

空间复杂度:最好情况:O(logN)   最坏:O(N)  (退化为冒泡排序)

稳定性:不稳定

说了这么些,经典的排序算法还有选择排序   归并排序    计数排序 等这么几个排序算法,这里限于篇幅,就下一篇再小结。

同时哪里有不对,也希望能给我指出。乐意讨论~~

原文地址:https://www.cnblogs.com/tp-16b/p/8555071.html

时间: 2024-11-05 14:53:15

经典排序方法及细节小结(1)的相关文章

javascript 数组排序sort方法和自我实现排序方法的学习小结 by FungLeo

前言 针对一个数组进行排序,一个很常见的需求.尤其在后端.当然,前端也是有这个需求的. 当然,数组排序,是有现成的方法的.就是sort()方法. 我们先开看下这个. 标准答案,sort方法 var arr = [45,98,67,57,85,6,58,83,48,18]; console.log('原数组'); console.log(arr); console.log('sort方法从小到大排序'); console.log(arr.sort(function(a,b){return a-b}

javascript 自己实现数字\字母和中文的混合排序方法 by FungLeo

javascript 自己实现数字\字母和中文的混合排序方法(纯粹研究,不实用) 前言 在上一篇博文<javascript 数组排序sort方法和自我实现排序方法的学习小结>中,我用自己的方法实现了数字数组的排序. 当然,实际运用中,我还是会使用sort方法更加方便.但是,我上一篇博文,仅仅是实现了数字排序,而srot方法默认可是能给字母实现排序的哦!而我的代码只能排序数字,看起来还是弱弱的. 所以,我得加上能排字母甚至中文的排序方法. 实现代码 $(function(){ var arr =

实例365(13)---------经典数组排序方法------选择排序法

一:使用选择排序法对一维数组进行排序,截图 /*选择排序的个人理解:第一遍筛选,选出其中最大的值,得到值和下标 将最大的值的位置和数组的第一个位置交换 从数组的第二个位置开始第二遍筛选 将其中最大的值的位置和数组的第二个位置交换 直到筛选完数组 */ 二:代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; u

十大经典排序算法小结

排序可以说是套路最多的基本算法了,今天来了兴致,那就总结一下这十大排序算法吧. 冒泡法: 这可以算是知名度最高的算法之一了吧,可以说不会这个算法都不好意思说自己写过代码.冒泡排序是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前面.这个过程类似于水泡向上升一样,因此而得名.不多说了,直接上代码: #include<iostream> #include<algorithm> using namespace std; #define LEN 6 int

经典算法之排序方法大全

经典排序算法之简单选择排序 http://m.blog.csdn.net/article/details?id=47321309 经典排序算法之冒泡排序 http://m.blog.csdn.net/article/details?id=47318573 经典排序算法之直接插入排序 http://m.blog.csdn.net/article/details?id=47321635 经典排序算法之希尔排序 http://m.blog.csdn.net/article/details?id=473

经典排序:冒泡排序+选择排序 小结

经典排序:冒泡排序+选择排序 例 FJUTOJ 1842 冒泡排序 原理是取相邻两个数进行大小比较,判断是否交换. 以从小到大排序为例,冒泡排序就像气泡一样,最小的数慢慢浮上来,最大的数慢慢沉下去.那么完整从头到尾做一次之后最后一位就是原序列中最大的数字了.然后只需要对1~(n-1)个数字进行排序,完成后倒数第二个数字也为原序列的1~n-1元素中最大的值.如此重复,进行n-1次一定能完成排序.参考代码: 1 #include <stdio.h> 2 void BubbleSort(int *,

C#实现所有经典排序算法

C# 实现所有经典排序算法 1 .选择排序 选择排序 原理: 选择排序是从冒泡排序演化而来的,每一轮比较得出最小的那个值,然后依次和每轮比较的第一个值进行交换. 目的:按从小到大排序. 方法:假设存在数组:72, 54, 59, 30, 31, 78, 2, 77, 82, 72 第一轮依次比较相邻两个元素,将最小的一个元素的索引和值记录下来,然后和第一个元素进行交换. 如上面的数组中,首先比较的是72,54,记录比较小的索引是54的索引1.接着比较54和59,比较小的索引还是1.直到最后得到最

常见经典排序算法

插入排序: 算法简介:接插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止.时间复杂度为O(n^2). 最稳定的排序算法但是效率很低 代码实现: void InsertSort(int *arr,int n) {                  for (int index = 0; index < n-1; ++index)                 {             

经典排序算法 - 归并排序Merge sort

经典排序算法 - 归并排序Merge sort 原理,把原始数组分成若干子数组,对每个子数组进行排序, 继续把子数组与子数组合并,合并后仍然有序,直到所有合并完,形成有序的数组 举例 无序数组[6 2 4 1 5 9] 先看一下每一个步骤下的状态,完了再看合并细节 第一步 [6 2 4 1 5 9]原始状态 第二步 [2 6] [1 4] [5 9]两两合并排序,排序细节后边介绍 第三步 [1 2 4 6] [5 9]继续两组两组合并 第四步 [1 2 4 5 6 9]合并完成,排序完成 输出结