内部排序算法汇总

本文介绍了五种内部排序算法:直接插入排序、希尔排序、冒泡排序、快速排序和选择排序。每种算法的基本思想、特点和时间复杂度都有所讲解,并提供了C语言的实现示例。此外,还提到了稳定性和适用场景,如快速排序是不稳定的,而冒泡排序和归并排序是稳定的。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

排序分为内部排序和外部排序两大类:

以下均为内部排序

直接插入排序

说明:

基本思想:每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。

例如,已知待排序的一组纪录r[10]是:

10,9,8,7,6,5,4,3,2,1

假设在排序过程中,前3个纪录已按关键码值递增的次序重新排列,构成一个有序序列:

8,9,10

将待排序纪录中的第4个纪录(即7)插入上述有序序列,以得到一个新的含4个纪录的有序序列。首先,应找到7的插入位置,再进行插入。可以将7放入一个临时变量中,这个变量称为监视哨,然后从10起从右到左查找,7小于10,将10右移一个位置,7小于9,又将9右移一个位置,以此类推,直到数组遍历完,此时7是最小的,8,9,10均有后移,因此,将7放到r[0]。假设7大于第一个值r[0]。它的插入位置应该在r[0]和r[1]之间,由于9已经右移了,留出来的位置正好留给7.后面的纪录依照同样的方法逐个插入到该有序序列中。若纪录数是n,则进行n-1趟排序,才能完成。

时间复杂度为O(n^2),是稳定的排序方法。

特点:比较和移动次数多

c实现:

/**
 * 直接插入排序
 */
#define MAXSIZE 10
void directInsertSort(int number[]) {
    int temp;
    for(int i = 1; i < MAXSIZE; i++) {
        temp = number[i];
        int j = i-1;
        for(; j >= 0; j--)
        {
            if(temp < number[j])
                number[j+1] = number[j];
            else
             break;
            
        }
        number[j+1] = temp;
    }
}

希尔排序

说明:

基本思想:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。

原理:从对直接插入排序的分析得知,其算法时间复杂度为O(n^2),但是,若待排记录序列为“正序”时,其时间复杂度可提高至O(n),由此,若待排记录序列按关键字“基本有序”,直接插入排序的效率就可大大提高,从另一方面来看,由于直接插入排序算法简单,则在n很小时效率也比较高。希尔排序正是从这两点分析出发对直接插入排序进行改进得到的一种插入排序方法。

例如,已知待排序的一组纪录r[10]是:

10,9,8,7,6,5,4,3,2,1

首先将该序列分成5个子序列{r[0],r[5]},{r[1],r[6]},{r[2],r[7]},{r[3],r[8]},{r[4],r[9]},分别对每个子序列进行直接插入排序,排序结果:

5,4,3,2,1,10,9,8,7,6

这称为一趟希尔排序,然后进行第二趟希尔排序,即分别对下列3个子序列{r[0],r[3],r[6],r[9]},{r[1],r[4],r[7]},{r[2],r[5],r[8]}进行直接插入排序,结果如下:

2,1,3,5,4,7,6,8,10,9

最后对整个序列进行一趟直接插入排序。至此,希尔排序结束,整个序列有序。

子序列的构成不是“逐段分割”,而是将相隔某个“增量”的记录组成一个子序列,上面第一趟增量5,第二趟增量3,第三趟增量1.(应使增量序列中的值没有除1以外的公因子,并且最后一个增量值必须等于1


时间复杂度最小为O(n^(3/2)),是稳定的排序方法。

c实现:

/**
 * 希尔排序
 */
#define MAXSIZE 10
void shellInsert(int number[],int dk) {
    
    int temp;
    for(int i = dk; i < MAXSIZE; i++)
    {
        temp = number[i];
        int j = i-dk;
        for (; j >= 0; j = j-dk) {
            
            if(temp < number[j])
                number[j+dk] = number[j];
            else
                break;
            
        }
        
        number[j+dk] = temp;
        
    }
    
}

void shellSort(int number[],int dlta[],int t) {
    for(int i = 0; i < t; i++) {
        shellInsert(number,dlta[i]);
    }
}

    int number[MAXSIZE] = {10,9,8,7,6,5,4,3,2,1};
    int dlta[3] = {5,3,1};
    shellSort(number,dlta,3);

冒泡排序:

说明:

基本思想:冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端,故名。

例如,已知待排序的一组纪录r[10]是:

10,9,8,7,6,5,4,3,2,1

首先将第一个记录和第二个记录比较,若为逆序,则交换,此时将10和9交换,然后将第二个记录和第三个记录比较,以此类推,直到最后两个记录做完比较。这个过程称为第一趟起泡排序。结果如下:

9,8,7,6,5,4,3,2,1,10

这样最大的数就浮出水面了,然后进行第二趟起泡排序,对前n-1个记录再进行同样的操作:以此类推,每一趟冒泡排序都会浮出一个最大数。

若初始序列为正序,则只需进行一趟排序,在排序过程中进行n-1次关键字的比较。

总的时间复杂度为O(n^2),是稳定的排序方法。

c实现:

/**
 * 冒泡排序
 */
#define MAXSIZE 10
#define SWAP(x,y) {int t; t = x; x = y; y = t;}
void bubbleSort(int number[]) {

    bool didSwap = false;
    
    for(int i = MAXSIZE-1; i > 0; i--) {
        
        for(int j = 0; j < i; j++)
        {
            if (number[j]>number[j+1]) {
                SWAP(number[j], number[j+1])
                didSwap = true;
            }
        }
        
        if (!didSwap) {
            return;
        }
    }
    
}

快速排序:

说明:

基本思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序。它是对冒泡排序的一种改进

例如,已知待排序的一组纪录r[7]是:

49,38,65,97,76,13,27

首先定义一个支点,这里我们选择第一个记录为支点,pivotkey=49;然后定义两个指针low和high,分别指向记录的两端的两个值;到这里初始化完成;

下面进行我们的第一趟快排;首先从high开始,和我们的pivotkey比较,如果大于pivotkey,则指针继续左移,直到high指向的值小于pivotkey,则将high指向的值放到原始的位置,也就是数组的第0个位置(第一个记录)结果如下:

27,38,65,97,76,13,()

然后从low开始,和我们的pivotkey比较,如果小于pivotkey,则指针继续右移,直到low指向的值大pivotkey,则将low指向的值放到上面空出来的位置,结果如下:

27,38,(),97,76,13,65

以此类推,直到low和high相等的时候,就是整个数组都已经和pivotkey进行比较了,此时再将pivotkey放到空出的位置中,也就是low和high指向的位置,结果如下:

27,38,13,(49),76,97,65

以上就是完成了一趟快排,此时数组分为两部分,49左边的都比49右边的小,再分别对两部分数组进行同样的快排;

时间复杂度O(nlog2n),是不稳定的排序方法。

c实现:

/**
 * 快速排序
 */
#define MAXSIZE 10
void quickSort(int number[]) {
    int low = 0;
    int high = MAXSIZE-1;
    QSort(number,low,high);
    
}

void QSort(int number[],int low,int high) {

    if (low < high) {
        int pivotLoc = partitionQSort(number,low,high);
        QSort(number,low,pivotLoc-1);
        QSort(number,pivotLoc+1,high);
    }
    
}

int partitionQSort(int number[],int low,int high) {
    
    int pivotkey = number[low];
    while (low < high) {
        while (low < high && number[high]>=pivotkey) {
            high--;
        }
        number[low] = number[high];
        while (low < high && number[low]<=pivotkey) {
            low++;
        }
        number[high] = number[low];
    }
    number[low] = pivotkey;
    return low;
}

选择排序:

说明:

基本思想:每一趟在n-i+1个记录中选择关键字最小的记录,并和第i个记录交换。

例如,已知待排序的一组纪录r[10]是:

10,9,8,7,6,5,4,3,2,1

首先将第一个记录和第二个记录比较,记录较小的值的序号为m=2(从1开始),然后将第m个记录和第三个记录比较,以此类推,直到m和最后一个记录做完比较。此时,第m个记录就是最小值,然后将第一个记录和第m个记录进行交换,这个过程称为第一趟选择排序。结果如下:

1,9,8,7,6,5,4,3,2,10

这样最小的数就沉下水底了,然后进行第二趟选择排序,从第二个记录开始对后n-1个记录再进行同样的操作:以此类推,每一趟选择排序都会下沉一个最小数。

和冒泡排序的区别:冒泡排序每次比较都会附带交换处理,而选择排序每次比较只会记录位置,每一趟排序才会附带一次交换处理。

特点:移动次数少

时间复杂度O(n^2),是不稳定的排序方法。

c实现:

/**
 * 选择排序
 */
#define MAXSIZE 10
#define SWAP(x,y) {int t; t = x; x = y; y = t;}
void selectSort(int number[]) {
    int i, j, m;
    for(i = 0; i < MAXSIZE-1; i++) {
        m = i;
        for(j = i+1; j < MAXSIZE; j++)
            if(number[j] < number[m])
                m = j;
        if( i != m)
            SWAP(number[i], number[m]) ;
    }
}


堆排序:

说明:

是对选择排序的改进,辅助空间为1

堆的定义:如果将序列对应的一维数组看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端节点的值均不大于(或不小于)其左,右孩子节点的值。则堆顶元素必为序列的最小值(或最大值)

基本思想:若在输出堆顶的最小值之后,使得剩余n-1个元素的序列重新建成堆,则得到n个元素的次小值,如此反复,便能得到一个有序序列,这个过程称为堆排序。

实现堆排序要解决两个问题:

1.如何由一个无序序列建立一个堆

2.如何在输出堆顶元素之后,调整剩余元素成为一个新的堆

例如,已知待排序的一组纪录r[10]是(本例实现从大到小排序):

10987654321

初始树:

     10

     /\

    9  8

   /\  /\

  7  65  4

 /\  /

3  21

然后将这个二叉树建成堆,从0开始计数,那么最后一个非终端节点是i = (MAXSIZE%2 == 1)?MAXSIZE/2:(MAXSIZE/2-1);上例中就是i=5,也就是6对应的节点,那么从第5个元素开始调整,6比1大,交换(此时因为1和6交换了,如果之前1还有叶子节点的话,由于破坏了6的左子树的"堆",则需进行和上述相同的调整,直至叶子节点)然后调整第4个元素,2比3小,都小于7,将2和7交换,以此类推,建立的初始堆如下:

      1

     /\

    2  4

   /\  /\

  3  65  8

 /\  /

10 79

此时堆顶就是最小元素,我们将它输出,将它和最后一个元素(第n-1)交换,也就是和9交换,这样,我们把前9个元素看成一个新的二叉树,重新建立堆,此时堆顶就是次小元素,我们再把它和第8(第n-2)个元素交换,再把前8个元素看成新的二叉树,再建立堆,以此类推就能得到一个有序的序列了。

时间复杂度O(n^2),是不稳定的排序方法,适用于n较大的情况。

c实现:

/**
 * 堆排序
 */
#define MAXSIZE 16
#define SWAP(x,y) {int t; t = x; x = y; y = t;}
//构造堆积树,这里堆积树是一个完全二叉树,因此根节点为s,左孩子必为2s+1,右孩子必为2s+2;
void heapAdjust(int number[],int start,int end)
{
    //此函数的前提是除number[start]以外,均满足堆的定义,本函数调整number[start],使number[start]-number[end]成为大顶堆
    int temp = number[start];
    for (int i = 2*start+1; i <= end; i *= 2) {
        if (i<end && number[i]<number[i+1]) {
            i++;
        }
        if (temp >= number[i]) {
            break;
        }
        number[start] = number[i];
        start = i;
    }
    
    number[start] = temp;
   
}

void heapSort(int number[])
{
    //将number建成大顶堆,最后一个非终端节点是i = (MAXSIZE%2 == 1)?MAXSIZE/2:(MAXSIZE/2-1);
    for (int i = (MAXSIZE%2 == 1)?MAXSIZE/2:(MAXSIZE/2-1) ; i >= 0; i--) {
        heapAdjust(number, i, MAXSIZE-1);
    }
    //此时堆顶为最小记录,将它和当前未排序子序列1-i中的最后一个记录交换,然后再将1..i-1重新调整为大顶堆
    for (int i = MAXSIZE-1; i > 0; i--) {
        SWAP(number[0], number[i]);
        heapAdjust(number, 0, i-1);
    }
}

归并排序:

说明:

基本思想:将两个或者两个以上的有序表组合成一个新的有序表。

2-路归并排序中的核心操作是将一维数组中前后相邻的两个有序序列归并为一个有序序列。

例如,已知待排序的一组纪录r[10]是:

10,9,8,7,6,5,4,3,2,1

一趟归并以后。结果如下:

[9,10][7,8][5,6][3,4][1,2]

二趟归并以后,结果如下:

[7,8,9,10][3,4,5,6][1,2]

以此类推,每一趟归并中的两个有序序列归并为一个有序序列的操作详细步骤是(如下partitionMSort函数的实现):首先有一个和原数组同样大小的辅助空间来存放待排序的记录,例如上面第三趟归并,待排序的两组有序序列是[7,8,9,10][3,4,5,6],辅助空间大小还是10,初始化为{7,8,9,10,3,4,5,6,0,0},设定两个指针(这里是位置变量start指向7,i=mid+1指向3),最初位置分别为两个已经排序序列的起始位置,比较两个指针所指向的元素7和3,选择相对小的元素放入到原数组相应位置,这里是从0位置开始,这里将3放到原数组number中的第0个位置并将并移动指针到下一位置,同时将i+1;指向4,继续上面的步骤,将start和i指向的元素7和4比较,一次类推,直到两个数组中有一个数组比较完了,也就是两个指针中有一个超出该序列,此时将另一序列剩下的所有元素直接复制到合并序列尾即可

时间复杂度O(nlogn),是稳定的排序方法。

c实现:

/**
 * 归并排序
 */
#define MAXSIZE 16
void mergeSort(int number[]) {
    MSort(number, 0, MAXSIZE-1);
}

void MSort(int number[],int start,int end) {
    if (start != end)
    {
        int mid = (start+end)/2;
        MSort(number, start, mid);
        MSort(number, mid+1, end);
        partitionMSort(number, start, mid, end);
    }
}

void partitionMSort(int number[],int start,int mid,int end) {
    
    int i,j;
    int temp[MAXSIZE] = {0};
    for (i = start; i<=end; i++) {
        temp[i] = number[i];
    }
    
    for (i = mid+1,j = start; start<=mid && i<=end; j++) {
        if (temp[start]<temp[i])
            number[j] = temp[start++];
        else
            number[j] = temp[i++];
        
    }
    
    if(start<=mid)
        for (; start <= mid; j++) {
            number[j] = temp[start++];
        }
    
    if(i<=end)
        for (; i <= end; j++) {
            number[j] = temp[i++];
        }
    
    for(i = 0; i < MAXSIZE; i++)
        printf("%d ", number[i]);
    printf("\n");
    
}

基数排序:

说明:

基本思想:基数排序是借助"分配""收集"两种操作对单逻辑关键字进行排序的一种内部排序方法

不需要进行记录关键字间的比较,它是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法

最低位优先:(Least Significant Digit first)简称LSD,就是从最次位关键字起进行排序,然后再对高一位关键字进行排序,依次重复,直到对最高位关键字排序后便成为一个有序序列。

最高位优先:(Most Significant Digit first)简称MSD,和LSD相反,从最主位(上面说的最高位)关键字开始排序。balabala


例如,已知待排序的一组纪录r[10]是:

12785273681991774630

第一趟分配对最低数位关键字(个位数)进行

结果如下:

 01 23 45 67 89

 30 91  12  73          46  77  78  19

        52                      68

第一趟收集结果如下:

30911252734677786819

第二趟分配对十位数进行:

 01 23 45 67 89

     12     30   46  52  68  73      91

     19                      77

                             78

第二趟收集结果如下:

12193046526873777891

至此排序完毕。

c实现:

/**
 * 基数排序
 */
#define MAX_SPACE 11
#define RADIX 10
#define MAX_NUM_OF_KEY 3
void lsdSort()
{
    int data[MAX_SPACE] = {73, 22, 93, 143, 55, 14, 28, 65, 39, 81,111};
    //    这里临时二维数组的大小,行数10应该和data的大小对应,因为有可能有某一位全是1或者几的情况,这样会造成空间浪费,因此可以使用链表来改善这个问题
    int temp[MAX_SPACE][RADIX] = {0};//这里假设不存在为0的记录
    int order[10] = {0};//用来记录每次分配后,相应组的记录个数
    //    这里是要比较数字的最大位数
    int weishu = MAX_NUM_OF_KEY;
    
    int m,n,i,x = 0,w = 1;

    while (weishu>0) {
        //分配
        for (i = 0; i<MAX_SPACE; i++) {
            x=(data[i]/ w)%10;
            temp[order[x]][x] = data[i];
            order[x]++;
            data[i] = 0;
        }
        
        i = 0;
        //收集
        for (m = 0; m<RADIX; m++)
        {
            for (n=0; n<order[m]; n++)
            {
                if (temp[n][m] != 0) {
                    data[i++]= temp[n][m];
                }
                temp[n][m]=0;//清空
            }
            order[m] = 0;//清空
        }
        w *= 10;
        weishu--;
    }
    
    printf("\n排序中:");
    for(i = 0; i < MAX_SPACE; i++)
        printf("%d ", data[i]);
}

由于基数排序所需的辅助空间太大,我们使用链表来改善这个问题,以下实现的是链式基数排序

链式基数排序的时间复杂度最小为O(d(n+rd)),是稳定的排序方法。因此它适用于n值很大而关键字较小的序列。(n是记录个数,每个记录含有d个关键字,每个关键字的取值范围是rd个值)

c实现:

/**
 * 链式基数排序,这里只实现思路
 */
#define MAX_SPACE 11
#define RADIX 10          //关键字基数,此时是十进制整数的基数
#define MAX_NUM_OF_KEY 8  //关键字项数的最大值

typedef struct{
    KeysType keys[MAX_NUM_OF_KEY]; //关键字
    InfoType otherItems; //其他数据项
    int next;
    
}SLCell; //静态链表的结点类型

typedef struct{
    SLCell r[MAX_SPACE]; //静态链表的可用空间,r[0]为头结点
    int keyNum; //记录的当前关键字个数
    int recNum; //静态链表的当前长度
    
}SLList;  //静态链表类型

typedef int ArrType[RADIX]; //指针数组类型

void distribute(SLCell &r,int i,ArrType &f,ArrType &e)
{
//    静态链表L的r域中记录已按(keys[0],...keys[i-1])有序。
//    本算法按第i个关键字keys[i]建立RADIX个子表,使同一个子表中记录的keys[i]相同
//    f[0..RADIX-1]和e[0..RADIX-1]分别指向各子表中第一个和最后一个记录。
    for (int j = 0; j<RADIX; ++j) {
        f[j] = 0;    //各子表初始化为空表
    }
    
    for (int p = r[0].next; p; p = r[p].next){
        j = ord(r[p].keys[i]);  //ord将记录中第i个关键字映射到[0..RADIX-1],
        if (!f[j] ) {
            f[j] = p;
        }
        else
        {
            r[e[j]],next = p;
        }
        e[j] = p;   //将p所指的结点插入第j个子表中
    }

}

void collect(SLCell &r,int i,ArrType f,ArrType e)
{
    //    本算法按keys[i]自小到大地将f[0..RADIX-1]所指各子表依次链接成一个链表,
    //    e[0..RADIX-1]为各子表的尾指针。
    for (int j = 0; !f[j]; j = succ(j));//找第一个非空子表,succ为求后继函数
    r[0].next = f[j]; //r[0].next 指向第一个非空子表中第一个结点
    int t = e[j];
    
    while(j < RADIX)
    {
        for ( j = succ(j); j<RADIX-1 && !f[j]; j = succ(j));//找下一个非空子表
        if (f[j]) {  //链接两个非空子表
            r[t].next = f[j];
            t = e[j];
        }
    }
    r[t].next = 0; //t指向最后一个非空子表中的最后一个结点
    
}

void radixSort(SLList &L)
{
//    L是采用静态链表表示的顺序表
//    对L作基数排序,使得L成为按关键字自小到大的有序静态链表,L.r[0]为头结点。
    for (int i = 0; i<L.recnum; ++i) {
        L.r[i].next = i+1;
    }
    L.r[L.recnum].next = 0; //将L改造为静态链表
    for (int i = 0; i<L.keynum; ++i) { //按最低位优先依次对各关键字进行分配和收集
        distribute(L.r,i,f,e); //第i趟分配
        collect(L.r,i,f,e); //第i趟收集
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值