【数据结构学习记录25】——排序与插入排序

零.排序的基本概念

1.定义

假设含n个记录的序列:{R1,R2,R3,···,Rn}
其相应的关键字序列为:
{K1,K2,K3,···,Kn}。
需确定1,2,3,···,n的一种排列p1,p2,p3,···,pn,使其相应的关键字满足如下的非递减(或非递增)关系
Kp1 ≤ Kp2 ≤K p3 ≤ ··· ≤ Kpn
即,使最上面的那个序列成为一个按关键字有序的序列:
{Rp1,Rp2,Rp3,···,Rpn}
以上中,如果排序的关键字是主关键字(唯一),则排序的结果一定是唯一的,如果次关键字,则排序的结构不唯一。

2.稳定性

针对于多关键字的排序方法,假设两个关键字相等。Ki = Kj,而且Ri 领先于Rj 也就是i < j 。若排序后:

  1. Ri 任然领先于Rj ,则该排序方法是稳定的。
  2. 反之,则不稳定。

如果证明一个排序是不稳定的,只需要找出一对K即可。

3.内外部排序

根据待排序记录的数量不同,使得排序过程中设计的储存器不同,可将排序分为两种:

  1. 内部排序:数据较少(仅相较于外部排序),放在计算机随机储存器(内存条)中,进行排序的过程
  2. 外部排序:就是数据太多,内存条存不下,在内部排序中,尚需要对外进行访问的过程。

一.直接插入排序

1.算法原理

插入排序的思想是:将一个表分成有序无序两个部分。我们每次将无序部分中的某个元素插入到有序部分中合适的位置。这就是基本插入排序的思想。
这种算法的时间复杂度为: O ( n 2 ) O(n^2) O(n2)

2.排序过程

在这里插入图片描述

3.代码实现

void InsertSort(int a[], int n)
{
    
    
    for(int i= 1; i<n; i++)
    {
    
    
        if(a[i] < a[i-1])
        {
    
    //若第 i 个元素大于 i-1 元素则直接插入;反之,需要找到适当的插入位置后在插入。
            int j= i-1;
            int x = a[i];
            while(j>-1 && x < a[j])
            {
    
      //采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间
                a[j+1] = a[j];
                j--;
            }
            a[j+1] = x;      //插入到正确位置
        }
    }
}

二.折半插入排序

1.算法原理

这种算法的思想和基本插入排序相同,也是有序无序两个部分。不过,通过二分查找的方式,查找我们的插入到有序部分中的下标。
折半插入排序算法相比较于直接插入排序算法,只是减少了关键字间的比较次数,而记录的移动次数没有进行优化,所以该算法的时间复杂度仍是 O ( n 2 ) O(n^2) O(n2)

2.排序过程

这里展示最后一次的排序过程:
在这里插入图片描述

3.代码

void BInsertSort(int a[],int size){
    
    
    int i,j,low = 0,high = 0,mid;
    int temp = 0;
    for (i=1; i<size; i++) {
    
    
        low=0;
        high=i-1;
        temp=a[i];
        //采用折半查找法判断插入位置,最终变量 low 表示插入位置
        while (low<=high) {
    
    
            mid=(low+high)/2;
            if (a[mid]>temp) {
    
    
                high=mid-1;
            }else{
    
    
                low=mid+1;
            }
        }
        //有序表中插入位置后的元素统一后移
        for (j=i; j>low; j--) {
    
    
            a[j]=a[j-1];
        }
        a[low]=temp;//插入元素
    }
   

三.2路插入排序

1.算法原理

这个算法和直接插入排序都差不多,只不过将插入的方向变成了两个,然后把链模拟成环链,然后就可以向前插,也可以向后插,这就是说的,两路。
2-路插入排序相比于折半插入排序,只是减少了移动记录的次数,没有根本上避免,所以其时间复杂度仍为 O ( n 2 ) O(n^2) O(n2)

2.排序过程

在这里插入图片描述

3.代码

void insert(int arr[], int temp[], int n)
{
    
    
    int i,first,final,k;
    first = final = 0;//分别记录temp数组中最大值和最小值的位置
    temp[0] = arr[0];
    for (i = 1; i < n; i ++){
    
    
        // 待插入元素比最小的元素小
        if (arr[i] < temp[first]){
    
    
            first = (first - 1 + n) % n;
            temp[first] = arr[i];
        }
        // 待插入元素比最大元素大
        else if (arr[i] > temp[final]){
    
    
            final = (final + 1 + n) % n;
            temp[final] = arr[i];
        }
        // 插入元素比最小大,比最大小
        else {
    
    
            k = (final + 1 + n) % n;
            //当插入值比当前值小时,需要移动当前值的位置
            while (temp[((k - 1) + n) % n] > arr[i]) {
    
    
                temp[(k + n) % n] =temp[(k - 1 + n) % n];
                k = (k - 1 + n) % n;
            }
            //插入该值
            temp[(k + n) % n] = arr[i];
            //因为最大值的位置改变,所以需要实时更新final的位置
            final = (final + 1 + n) % n;
        }
    }
    // 将排序记录复制到原来的顺序表里
    for (k = 0; k < n; k ++) {
    
    
        arr[k] = temp[(first + k) % n];
    }
}

四.表插入排序

1.算法原理

我们通过指向来表示一个元素的下一个结点来表示插入,有点像模拟链表的方式。这样,我们的插入并需不要移动,只需要更新next指针。缺点就是耗内存。
我们从MAX的next开始,末尾指向MAX,也就是链表头尾结点公用Max。
最后再重新遍历一遍Next数组,然后修改原排序数组即可。
从表插入排序的实现过程上分析,与直接插入排序相比只是避免了移动记录的过程(修改各记录结点中的指针域即可),而插入过程中同其它关键字的比较次数并没有改变,所以表插入排序算法的时间复杂度仍是 O ( n 2 ) O(n^2) O(n2)

2.排序过程

在这里插入图片描述
在这里插入图片描述

3.代码实现

#include <stdio.h>
#include <stdlib.h>

#define     MAX        0xffff

void insert(int arry[], int size)
{
    
    
    int *next = (int*)malloc(sizeof(int) * (size+1));   // 这是next数组
    int *key = (int*)malloc(sizeof(int) * (size+1));    // 这是模拟表。表头默认为最大。
    int i, pre, insert, temp;

    next[0] = 0;    // 把表头指向表尾
    key[0] = MAX;   // 把表头值设为为最大

    for (i = 0; i < size; ++i)  
    {
    
    
        // 构造新的key表,并且将next数组初始化(指向表尾)
        key[i+1] = arry[i];
        next[i+1] = 0;
    }

    insert = 1; // 从新的模拟表非表头开始
    while(insert < size+1)  // 把原表全部插入
    {
    
    
        pre = 0;    // 初始化表结点指针(前驱)指向表头,当然是0.
        // 循环终止的条件: 当前表结点的下一个是表尾 或者 当前待插入的值小于当前表结点的下一个(前驱的后继才是我们要比较的值),
        while(next[pre] != 0 && *(key+next[pre]) <= key[insert])
        {
    
    
            pre = next[pre];    // 更新表指针
        }
        // 此时,我们表结点的后继就是我们要插入的值。该过程和链表的插入一模一样
        temp = next[pre];       // 把表结点的指向的值存起来
        next[pre] = insert;     // 把表结点指向插入的地址
        next[insert] = temp;    // 把插入的结点指向刚才表结点的后继
        ++insert;               // 更新待插入的指针
    }

    // 将排好序的结果 赋值给原数组
    pre = next[0];
    for (i = 0; i < size; ++i)
    {
    
    
        arry[i] = key[pre];
        pre = next[pre];
    }

}


int main()
{
    
    
    int a[8] = {
    
    7,6,4,2,5,1,3};
    int i = 0;
    insert(a, 7);
    for (i = 0; i < 7; ++i)
    {
    
    
        printf("%d ", a[i]);
    } 
    return 0;
}

五.希尔排序

1.算法原理

先将整个带排序记录序列分成若干个子序列,分别进行插入排序。待整个序列中的记录“基本有序”时,再对全体进行一次直接插入排序。
换句话说,就是将一个大的序列,分成小组进行直接排序,在小组内容够少的情况下,插入并不需要移动较多元素。然后逐渐扩大规模,让小组渐渐变大,因为刚才进行过一次交换排序,所以该序列的有序度会提高,所以插入可能并不需要移动太多,直到最后对整体进行直接插入,保证全部排好序。虽然如此,在最坏情况下,它的时间复杂度任然为: O ( n 2 ) O(n^2) O(n2)

2.分组数目与时间复杂度

希尔排序的时间复杂度,是取决于增量序列的函数,这个涉及一些数学上还未解决的难题。所以,目前没有人能得到最好的增量序列。但是已经研究出一些局部结论。当增量序列等于 d l t a [ k ] = 2 t − k + 1 − 1 dlta[k]=2^{t-k+1}-1 dlta[k]=2tk+11 时,时间复杂度大约在 O ( n 1.5 ) O(n^{1.5}) O(n1.5)
增量序列有各种取法,但是必须遵守:增量序列中的值没有除1以外的公因子,且最后一次的增量一定是1.

3.排序过程

为了程序方便,我们可以自定义增量序列,也可以用长度÷2的方式,比如长度是10,那么第一次增量为10÷2=5;然后就是增量5÷2=2,然后就是增量为2÷2=1,完成。
这个例子为了直观,我们采用自定义增量序列的方式,序列dlta={3,2,1}
相同颜色块进行直接插入排序,图中是dlta为x时,已排好的状态。
在这里插入图片描述

4.代码实现

和上面一模一样
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>

void ArryShow(int arry[], int size);
void Sinsert(int arr[], int size, int dlat);
void ShellSort(int arry[], int arrysize, int dlat[], int dlatsize);

void Sinsert(int arr[], int size, int dlat)
{
    
    
    int i, pre, pptr, this, tptr, temp;

    for(int i=dlat;i<size;i++)
    {
    
    
        int j = i;
        int temp = arr[j];
        if(arr[j]<arr[j-dlat]){
    
    
            while(j-dlat>=0 && temp<arr[j-dlat])
            {
    
    
                //移动法
                arr[j] = arr[j-dlat];
                j-=dlat;
            }
            arr[j] = temp;
        }
    }
}

void ShellSort(int arry[], int arrysize, int dlat[], int dlatsize)
{
    
    
    int i = 0;
    for(; i < dlatsize; ++i)
    {
    
    
        Sinsert(arry, arrysize, dlat[i]);
        ArryShow(arry, arrysize);
    }
}

void ArryShow(int arry[], int size)
{
    
    
    int i = 0;
    printf("\n");
    for (; i < size; ++i)
    {
    
    
        printf("%d ", arry[i]);
    }
}

int main()
{
    
    
    int a[8] = {
    
    7,6,4,2,5,1,3};
    int dlat[3] = {
    
    3,2,1};
    ShellSort(a, 7, dlat, 3);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/u011017694/article/details/111318983