数据结构知识整理 - 排序算法(本篇包括折半插入排序、快速排序以及堆排序)

版权声明: https://blog.csdn.net/Ha1f_Awake/article/details/84980250

主要内容


 

前提

排序是计算机程序设计中的一种重要操作,在很多领域中都有着广泛的应用。

排序的一个主要目的是便于查找。在前两篇关于查找的博文中提到的折半查找(要求有序的顺序表)和树表查找(二叉排序树结构)都涉及到排序算法。

人们设计了大量的排序算法以满足不同的需求。

著名计算机科学家D.E.Knuth在他的巨著《计算机程序设计艺术》第三卷《排序与查找》中,给出了25种排序方法,然而这还只是现有排序方法中的冰山一角。(人类真是nb......)


初步认识排序

将“无序”的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。

排序又分为内部排序外部排序:若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序;反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。

内部排序是一个逐步扩大记录的有序序列长度的过程。在排序过程中,可以将排序记录区分为两个:有序序列区无序序列区使有序序列区中记录的数目增加一个或几个的操作称为一趟排序

常见的内部排序可分类为:

1)插入类:将无序子序列中的一个或几个记录“插入”到有序序列中。主要包括直接插入排序折半插入排序希尔排序

2)交换类:通过“交换”无序序列中的记录,从而得到其中关键字最大或最小的记录,并将它加入有序子序列中,以此方法增加记录的有序子序列长度。主要包括冒泡排序快速排序

3)选择类:从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入有序子序列中,以此方法增加记录的有序子序列长度(没错,跟交换类的描述几乎相同)。主要包括简单选择排序树形选择排序堆排序

4)归并类:通过“归并”两个或两个以上的记录的有序子序列,逐步增加有序序列长度。常见有2-路归并排序

5)分配类:唯一一种不需要比较关键字的排序方法,排序时主要利用分配和收集两种基本操作来完成。常见有基数排序

排序还可以根据稳定性分为两类;

1)稳定排序:

假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法是稳定的。其中冒泡排序直接插入排序折半插入排序基数排序归并排序等属于稳定排序。

2)不稳定排序:

与稳定排序的结果相反则为不稳定排序。其中选择类排序快速排序希尔排序属于不稳定排序。

(本篇对于各排序算法的时间复杂度和空间复杂度暂不作分析)


待排序记录表的结构定义

(示例代码中的待排序记录均以顺序表为存储方式,且关键字设为整型数据)

#define MAXSIZE 20    /*顺序表的最大长度*/

typedef int KeyType   /*定义关键字类型为整型,即KeyType key等价于int key*/

/*与构造查找表类似*/
typedef struct
{
    KeyType key;
    OtherType other;
} RecordType;         /*记录类型*/

typedef struct
{
    RecordType rcds[MAXSIZE+1]    /*rcds[0]用作监视哨或闲置,可回顾线性表查找*/
    int length;
} SqList;                         /*顺序表类型*/

 

折半插入排序(Binary Insertion Sort)

插入排序的基本思想:每一趟排序中,将一个待排序记录按其关键字大小插入到“有序”记录的适当位置,直到所有待排序记录全部插入为止。

直接插入排序是最简单的排序方法,它采用顺序查找表查找待排序记录在有序序列上的插入位置,而“查找”操作可利用“折半查找”来实现,以“折半查找法”查找插入位置的排序则称为折半插入排序

<思路>

1)rcds[0]作监视哨或闲置,r[1]只有一个记录,不需要排序,所以排序从rcds[2]开始;

2)比较rcds[1]和rcds[2]的关键字大小,若rcds[1]的关键字大于rcds[2],则将rcds[1]后移一位,将首元素的位置让给rcds[2];反之,若rcds[2]的关键字更大,则保持rcds[2]原来的位置;

3)同(2)可以推理得出,若带插入记录rcds[i]的关键字大于rcds[j],小于rcds[j+1],则将rcds[j+1]到rcds[i-1]的元素后移一位,让出rcds[j+1]的位置给rcds[i];反之,若rcds[i]的关键字大于有序序列中的所有记录,则保持rcds[i]原来的位置;

4)由(3)可知,rcds[0]不仅可用作监视哨,还可以暂存待排序记录的信息。

代码如下(可回顾“折半查找”):

void Bin_Insert_Sort(SqList &L)
{
    for(int i = 2; i <= L.length; i++)    /*从rcds[2]开始插入排序*/
    {
        L.rcds[0] = L.rcds[i];            /*设置监视哨并暂存待排序记录的信息*/

        int low = 1, high = i - 1;        /*折半查找的范围是从1到i-1*/

        while(low <= high)                /*折半查找rcds[i]的插入位置*/
        {
            int mid = (low + high) / 2;

            if(L.rcds[0].key < L.rcds[mid].key) high = mid - 1;
            else low = mid + 1;
        }

        /*经过折半排序可将rcds[i]的位置确定在rcds[high+1],在草稿纸上推理一下*/

        for(int j = i-1; j >= high+1; j--) /*关键字更大的记录后移一位*/
            L.rcds[j+1] = L.rcds[j]; 

        L.rcds[high+1] = L.rcds[0];        /*插入*/
}

快速排序(Quick Sort)

交换排序的基本思想:两两比较待排序记录的关键字,一旦发现两个记录不满足次序要求时则进行交换,直到整个序列全部满足要求为止。

冒泡排序(Bubble Sort)是一种最简单的交换排序方法,它通过两两比较相邻记录的关键字,逆序则交换,从而使关键字较小的记录如气泡一般逐渐往上“漂浮”(左移),或者使关键字较大的记录如石块一般“沉落”(右移)。从右往左进行交换更接近“冒泡”的含义,而从左往右交换更像是“沉石”的过程。(可回顾“这里”)

在冒泡排序中,需要安排一个变量flag表示排序循环结束的标志。flag = 0表示在本趟排序中没有发生“交换”,即排序已经完成;反之,若flag = 1,继续进行排序。

快速排序由冒泡排序改进得到。冒泡排序中只能比较相邻的记录,所以每次“交换”只能消除一个逆序。而快速排序能够比较两个不相邻的记录,通过一次“交换”消除多个逆序,从而大大加快排序的速度。

<逻辑思路>

1)在待排序的n个记录中选择任意一个记录(通常选择第一个记录)作为枢轴(支点),设其关键字为pivotkey。经过一趟排序后,所有关键字小于pivotkey的记录“交换”到序列前面,所有关键字大于pivotkey的记录交换到序列后面,最后将枢轴记录插在中间;

2)不断重复上述过程,直至每一部分只包含一个记录。

<实现思路>

1)在待排序序列首尾设置指针low、high。初始化时,指针low指向rcds[1],指针high指向rcds[L.length];

2)将rcds[1]的信息暂存在rcds[0],此时rcds[1]包含的记录作为枢轴,rcds[1]的位置作为空位

3)指针low从左往右查找关键字大于pivotkey的记录,指针high从右往左查找关键字小于pivotkey的记录;

4)因为空位在前半部分,所以指针high首先开始查找关键字小于pivotkey的记录。假设找到的是rcds[n]上的记录,找到后将其记录存放在空位,存储后空位便由rcds[1]改为rcds[n],即空位出现在后半部分;

5)指针low开始查找关键字大于pivotkey的记录,并将记录存放在后半部分的空位,同时“生成”新的空位;

6)不断重复(4)(5)的过程,直至low == high,而且此时rcds[low](或rcds[high])正是枢轴记录的存放位置。

7)对前后部分分别重复上述操作,直至所有部分都只包含一个记录(递归)。

/*--------在一趟排序中切分前后部分,并返回枢轴位置--------*/

int Slice_n_Pivot(SqList &L, int low, int high)
{
    L.rcds[0] = L.rcds[1];            /*暂存枢轴记录*/
    int pivotkey = L.rcds[1].key;     /*初始化pivotkey*/

    while(low < high)                 /*当low未与high重合时*/
    {
        /*指针high从右往左移动,找到(第一个)关键字小于pivotkey的记录*/
        while((low < high) && (L.rcds[high].key >= pivotkey)) high--;    
        
        L.rcds[low] = L.rcds[high];   /*将找到的记录存放在前半部分的空位,同时得到新的空位*/

        /*指针low从右往左移动,找到(第一个)关键字大于pivotkey的记录*/
        while((low < high) && (L.rcds[low].key <= pivotkey)) low++;

        L.rcds[high] = L.rcds[low];
    }

    L.rcds[low] = L.rcds[0];          /*或L.rcds[high] = L.rcds[0];*/

    return low;               /*返回枢轴的位置,作为前半部分指针high和后半部分指针low的依据*/
}

       
/*----------在前后部分中递归-----------*/

void Recursive(SqList &L, int low, int high)
{
    if(low < high)
    {
        pivot = Slice_n_Pivot(L, low, high);

        /*因为在每次递归中low与high的值都会发生相应的变化,所以不需要再初始化实参*/       
        Recursive(L, low, pivot-1);    /*在前半部分递归排序*/

        Recursive(L, pivot+1, high);   /*在后半部分递归排序*/
    }
}

/*-----------总结快速排序------------*/

void Quick_Sort(SqList &L)
{
    Recursive(L, 1, L.length);
}

堆排序(Heap Sort)

选择排序的基本思想:每一趟从待排序记录中选出关键字最小的记录,按顺序放在已排序的记录序列最后,直到全部记录排序好为止。

堆排序是一种树形选择排序,在排序过程中,将存储待排序记录的顺序表看成是一棵完全二叉树(区别满二叉树)的顺序存储结构。利用完全二叉树中父结点与子结点的内在关系,在当前无序的序列中选择关键字最小(或最大)的记录。

<条件>

1)堆的定义:

以任意结点为根结点,根结点的关键字都大于(或小于)左、右子树的根结点。

若堆顶记录的关键字最大,则称堆为大根堆,反之称小根堆

2)完全二叉树的定义:

所有序号大于[n/2](n为结点数)的结点都是叶结点。

因此只需要依次将以[n/2]、[n/2-1]、...、[1]的结点作为根结点的子树调整为堆即可。

<思路>

(以大根堆为例)

1)调整一个小堆:

设根结点为rcds[s],左、右子树根结点分别为rcds[2s]、rcds[2s+1]。

先比较左、右子树根结点的关键字,若较大值为右子树根结点,则右子树根结点与根结点比较。若根结点的关键字更大,则该小堆满足要求;若左、右子树根结点中较大的关键字更大,则交换两个结点。

但是,交换结点后,可能会出现(原)根结点的关键字小于(原)右子树根结点的左、右子树根结点(rcds[2(2s+1)]、rcds[2(2s+1)+1]))的情况,这时候需要继续进行调整,即需要循环或递归。

2)利用完全二叉树的特性,从[n/2]结点开始往前调整小堆;

3)经过上面两步只能使一个无序序列刚好构成堆的条件,还未能称为有序序列。但我们可以利用堆的特性,将每次调整完得到的堆顶记录(关键字最大)放到最后(依次由[L.length]、[L.length-1]、...),这样就能得到一个从左往右,关键字从小到大的有序序列。

/*------------调整一个小堆-------------*/

void Adjust_Small_Heap(SqList &L, int s, int m)    /*s为调整根结点,m为序号最大的结点*/
{
    L.rcds[0] = L.rcds[s];        /*暂存根结点的记录*/

    for(int i = 2 * s; i <= m; i *= 2)     /*定位结点的左子树根结点*/
    {
        if((j < m) && (L.rcds[i] < L.rcds[i+1])) i++;    /*如果左小于右,用i表示右*/

        if(L.rcds[0] > L.rcds[i]) break;   /*如果根大于较大的结点,满足堆的条件,跳出循环*/

        L.rcds[s] = L.rcds[i]; s = i;      /*如果没有在上一步跳出循环,交换结点后继续判断是否满足堆的条件*/

    }

    L.rcds[s] = L.rcds[0];    /*经过上面的循环后,s表示的位置可能已经改变,需要重新插入(原)根结点的记录*/

}


/*----------利用完全二叉树的特性------------*/

void UseCBT(SqList &L)            /*CBT->Completed Binary Tree,完全二叉树*/
{
    int m = L.length;

    for(int s = n/2; s >=1; i--)
        Adjust_Small_Heap(L, s, m);
}


/*-------------将大根堆有序化--------------*/

void Heap_Sort(SqList &L)
{
    UseCBT(L);                    /*将无序序列转化成大根堆*/

    for(int i = L.length; i > 1; i--)
    {
        int t = L.rcds[1];        /*将堆顶记录往后存放*/
        L.rcds[1] = L.rcds[i];
        L.rcds[i] = t;

        Adjust_Small_Heap(L, 1, i-1);    /*交换结点后需要调整以rcds[1]为根结点的堆*/
    }
}

路过的圈毛君:“......”

猜你喜欢

转载自blog.csdn.net/Ha1f_Awake/article/details/84980250