8.内部排序

8.1 插入排序

8.1.1 直接插入排序
  1. 思路:

    ①从前面的有序子表中查找出待插入元素应该被插入的位置;

    ②给插入位置腾出空间,将待插入元素复制到表中的插入位置。

  2. 代码

    void InsertSort(ElemType a[], int n){
        for(int i = 2; i <= n; i++){            //将a[2]~a[n]插入前面已经排好的序列
            if(a[i] < a[i-1]){                  //若a[i]关键码小于前驱,将a[i]插入有序表
                a[0] = a[i];                    //复制为哨兵,a[0]不存放元素
                for(int j = i-1; a[0] < a[j]; --j)  //从后往前查找插入位置
                    a[j+1] = a[j];              //向后挪位
                a[j+1] = a[0];                  //复制到插入位置
            }
        }
    }
    
  3. 性能分析

  • 空间复杂度:仅使用了常数个辅助单元,空间复杂度为O(1)。

  • 时间复杂度:

    • 最好情况下,表中元素已经有序,每次插入元素都只需要比较一次而不需要移动元素,因而时间复杂度为O(n)。
    • 最坏情况下,表中元素逆序,总的比较次数和移动次数达到最大,时间复杂度为O(n^2)。
    • 平均情况下,时间复杂度为O(n^2)。
  • 稳定性:由于每次插入元素总是从后向前先比较再移动,不会出现相同元素相对位置发生变化的情况,因此直接插入排序是一个稳定的排序方法。

  • 适用性:适用于顺序存储和链式存储。为链式存储时,可以从前往后查找指定元素的位置。适用于基本有序和数据量不大的排序

8.1.2 折半插入排序
  1. 思路:当排序表为顺序表时,查找有序表时可以用折半查找来实现。

  2. 代码

    void BinInsertSort(ElemType a[], int n){
        int low,high,mid;
        for(int i = 2; i <= n; i++){            //将a[2]~a[n]插入前面已经排好的序列
                a[0] = a[i];                    //复制为哨兵,a[0]不存放元素
                low = 1;    high = i-1;         //设置折半查找的范围
                while(low <= high){             //折半查找
                    mid = (low + high)/2;       //取中间点
                    if(a[mid] > a[0])   high = mid - 1; //查找左半子表
                    else    low = mid + 1;      //查找右边半子表
                }
                for(int j = i-1; j >= high + 1; --j)  //从后往前查找插入位置
                    a[j+1] = a[j];              //向后挪位
                a[j+1] = a[0];                  //复制到插入位置
            }
        }
    }
    
  3. 性能分析

  • 空间复杂度:仅使用了常数个辅助单元,空间复杂度为O(1)。

  • 时间复杂度:减少了比较的次数,该比较次数与待排序表的初始状态无关,仅取决于n。移动次数并未改变。

  • 平均情况下,时间复杂度为O(n^2)。

  • 稳定性:稳定

8.1.3 希尔排序
  1. 过程:

    ① 取一个小于n的步长d1,把表中的全部记录分成d1组,左右距离为d1倍数的元素放在同一组,在各组内进行直接插入排序;

    ② 然后取第二步长d2小于d1,重复①,知道所取到的步长为1,即所有记录在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故可以很快得到最终结果。

  2. 代码

    void ShellSort(ElemType a[], int n){
        for(dk = n/2; dk >= 1; dk = dk/2)
            for(i = dk + 1; i <= n; ++i)
                if(a[i] < a[i - dk]){
                    a[0] = a[i];
                    for(j = i - dk; j > 0 && a[0] < a[j]; j-=dk)
                        a[j+dk] = a[j0];
                    a[j+dk] = a[0];
                }
    }
    
  3. 性能分析

  • 空间复杂度:仅使用了常数个辅助单元,空间复杂度为O(1)。

  • 时间复杂度:依赖于增量序列的函数

    • 当n在某个特定范围,时间复杂度为O(n^1.3)

    • 最坏情况下, 时间复杂度为O(n^2)

  • 稳定性:不稳定,相同关键字的记录被划分到不同的子表

  • 适用性:仅适用于线性表为顺序存储的情况。

8.2 交换排序

8.2.1 冒泡排序
  1. 基本思想:从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换他们,知道序列比较完。我们称为第一趟冒泡,结果是将最小的元素交换到待排序序列的第一个位置(或者将最大的元素交换到待排序列的最后一个位置)。下一趟冒泡时,前一趟所确定的最小的元素不在参与比较,没让冒泡的结果是把序列中的最小元素放到了最终的位置。这样,最多n-1趟冒泡救恩那个排好序。

  2. 伪代码

    void BubbleSort(ElemType a[], int n){
        for(int i = 0; i < n-1; i++){
            flag = false;              //表示本趟冒泡排序是否发生交换的标志
            for(int j = n-1; j > i; j--) //一趟冒泡排序
                if(a[j-1] < a[j]){     //若为逆序
                    swap(a[j-1],a[j]); //交换
                    flag = true;
                }
            if(flag == false)
                return ;               //本趟遍历后没有发生交换,说明已经有序
        }
    }
    
  3. 性能分析

  • 空间复杂度:仅使用了常数个辅助单元,空间复杂度为O(1)

  • 时间复杂度:

    • 最好情况下,初始序列有序,第一趟冒泡排序后,flag依然为false,比较次数n-1,移动次数为0,时间复杂度为O(n)

    • 最坏情况下,初始序列为逆序,共需要n-1趟排序,第i趟要进行n-i趟关键字的比较,每次比较都需要移动三次交换元素位置。

    • 比较次数n(n-1)/2,移动次数3n(n-1)/2,时间复杂度为O(n2),平均时间复杂度也为O(n2)。

8.2.2 快速排序
  1. 算法思想:快排的思想是基于分治法的:在待排序序列中任取一个元素pivot作为枢轴,通过一趟排序将待排序表划分为独立的两部分,前面的所有元素小于pivot,后面的元素都大于pivot。pivot被放在了最终的位置,这个过程称为一趟快排。然后两个子表递归重复上述过程,直至每部分只有一个元素或空为止,即所有元素被放在了最终的位置上。

  2. 伪代码

    void QuickSort(ElemType a[], int low, int high){
        if(low < high){                 //跳出递归的条件
            int pivotpos = Partition(a,low,high);   //将待排序列划分为两个子表
            QuickSort(a,low,pivotpos-1);            //对两个子表进行递归排序
            QuickSort(a,pivotpos+1,high);
        }
    }
    
  3. 性能分析

  • 空间效率:快排序是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,容量与递归调用的最大深度一致。

    • 最好情况下为O(log(2)n)

    • 最坏情况下要进行n-1次递归调用,栈的深度O(n)

    • 平均情况下O(log(2)n)

  • 时间效率:快排的运行时间与划分是否对称有关,

    • 最坏情况,每层递归,两个区域包含n-1个元素和0个元素,初始序列基本有序或基本逆序时,最坏情况下时间复杂度为O(n^2)

    • 最理想情况下,每个做到最平衡的划分,得到的两个子序列大小都不大于n/2,在这种情况下快排的序列将大大提升,时间复杂度为O(nlog(2)n)

    • 平均情况下,运行时间接近于最佳情况,时间复杂度为O(nlog(2)n)。

  • 稳定性:不稳定

8.3 选择排序

8.3.1 简单选择排序
  1. 思路:每一趟在后面n-i+1个待排序元素中选择关键字最小的元素,作为有序子序列的第i个元素,知道n-1趟做完,待排序序列只剩下1个,就不中再选了。

  2. 伪代码

    void SelectSort(ElemType a[], int n){
        for(int i = 0; i < n-1; i++){           //移动进行n-1趟
            min = i;                            //记录最小元素位置
            for(j = i + 1; j < n; j++)          //在a[i……n-1]中选择最小的元素
                if(a[j] < a[min])   min = j;    //更新最小元素的位置
            if(min != i)    swap(a[i],a[min])   //封装的swap()共移动元素三次
        }
    }
    

3.性能分析

  • 空间效率:仅使用了常数个辅助单元,空间复杂度为O(1)

  • 时间效率:最好的情况,已经有序,移动0次;但元素的比较次数与序列初始状态无关,始终是n(n-1)/2次,因此时间复杂度是O(n^2)

  • 稳定性:不稳定

8.3.2 堆排序
  1. 算法思想
  2. 伪代码
  3. 性能分析
  • 空间效率:仅使用了常数个辅助单元,空间复杂度为O(1)

  • 时间效率:建堆时间O(n),每次调整的时间是O(h),时间复杂度是O(n^2)

  • 稳定性:不稳定

8.4 归并排序
  1. 伪代码
  2. 性能分析
  • 空间效率:merge()操作中,辅助空间n个单元,空间复杂度为O(n)

  • 时间效率:每趟归并的时间复杂度为O(n),共需要log(2)n趟,时间复杂度为O(nlog(2)n)。

  • 稳定性:merge不会改变相同关键字记录的相对次序,是一种稳定的算法。

  • N个元素进行k路归并,排序的趟树满足k^m=N,从而m=log(k)N,向上取整。

8.5 基数排序
  1. 性能分析

  • 空间效率:一趟排序需要的辅助空间为r(r个队列,r个头指针,r个尾指针),但以后会重复利用到这些队列,所以技术排序的空间复杂度为O(r)

    • eg:0,1,2,3,4,5,6,7,8,9.r=10
  • 时间效率:技术排序共需要d趟分配和收集,一趟分配需要O(n),一趟收集需要O(r),所以技术排序的时间复杂度为O(d(n+r)),它与序列的初始状态无关。

    • eg:个十百千,d=4
  • 稳定性:对于基数排序算法而言,很重要一点就是按位排序时是稳定的。因此,这也保证了基数排序的稳定性

8.6 各种算法的比较
posted @ 2021-12-07 15:03  某科学的撒把豆子  阅读(34)  评论(0)    收藏  举报