【考研】C语言排序和查找算法总结

更新历史:

  • 2021年3月21日完成初稿

声明:本文部分图片来源于网络,如若侵权,请联系作者删除。

1.概论

  在程序设计中,经常使用数组来保存大量的数据,当对数据进行增加、删除、更新、查找操作时经常会使用到排序和查找算法对数组中的元素进行排序和查找。排序 (sorting) 是把一系列无序的数据按照特定的顺序(升序或者降序)重新排列为有序序列的过程,而查找 (searching) 则是在一种特定的数据结构中搜索一个特定元素的处理过程。本文将介绍并实现众多的排序和查找算法并给予总结,比较各种排序和查找算法的时空效率。

1.1 排序算法分类

  一个排序算法可以使得序列中的元素具有一定的有序结构,比如序列中有n个记录<a1, a2, ..., an>,通过排序希望得到一个升序(ascending order)或者降序(descending order)序列,这种有序序列有利于数据的增删改查操作的进行。在本文中将结合学生成绩管理系统讨论一些内部排序算法,内部排序是一种待排序列完全存放在内存中所进行的排序过程,相应的外部排序算法超出了讨论的范围。根据排序的思想,可将各类内部排序算法分为以下几类:

  1. 交换排序
    包括 冒泡排序(气泡排序)和 快速排序(快排);

  2. 选择排序
    包括 简单选择排序 和 堆排序 ;

  3. 插入排序
    包括 直接插入排序 、折半插入排序 和 希尔排序;

  4. 归并排序
    常用的有 二路归并排序;

  5. 基数排序
    不基于关键字比较的算法。

1.2 查找算法分类

  查找算法是基于用户所关心的关键字或查找条件而对序列中元素的查找,查找的结果是有可能查找到,也有可能没有找到,这是取决于查找条件的。同时,查找算法不仅仅在一个线性结构(如数组等)中进行查找,也可能在一定的非线性结构(如树、图)中查找,它们的实现方式不同,但原理是类似的,本文将主要叙述线性结构的查找,特别是基于数组等结构的查找。一定程度上,如果序列是有序的,那么查找序列中的元素时是容易的,这也是先叙述排序算法的原因之一。根据查找的思想,可将各类查找算法分为以下几类:

  1. 顺序查找

  2. 折半查找

  3. 分块查找

1.3 算法效率

  算法效率的度量一般是通过时间复杂度空间复杂度来描述的。算法的效率是和算法处理数据的规模(记为n)有关的,因此算法的运行时间和占用内存是和n有关的,是n的函数,通常用O(f(n))来表示,其中f(n)是实际的运行时间。用O(f(n))来表示近似的运行时间的意义在于O(f(n))表示了实际运行时间f(n)的上界,这个上界表示运行该算法所需最多的时间。比如看下面求解前n项和的算法主体:

for(i = 1; i <= n; i++)
{
    sum += i ;
}

  可以发现,执行语句sum += i ;花费了主要的时间,而且可以由循环条件看出这条语句会被执行n次,那么执行语句sum += i ;一次要花费时间t,那么就执行语句sum += i ;花费了大概nt的时间,加上循环判断语句的执行时间,该函数的执行时间可以看成是n的一阶线性函数f(n),总是可以找到另一个n的一阶线性函数g(n)使得g(n)f(n)的上界,那么略去g(n)的因子,只保留其阶数,那么就可以用O(n) 来表示。

  不过对于时间复杂度和空间复杂度的不理解不是那么至关重要的,把它理解为“语句执行次数”或者“问题的阶数”是不影响下面的内容的。

1.4 示例程序

  本文将基于学生成绩管理程序来介绍常用的排序和查找算法及其总结,首先简单介绍一下学生成绩管理程序,学生成绩管理程序记录学生的姓名和考试成绩,并基于考试成绩对学生进行排序,或者根据学生姓名来查找学生信息,其程序主体和输入的数据有:

#include <stdio.h>
#include <string.h>
#define MAXSIZE 30
typedef struct student
/* student information */
{
    char name[10] ;
    int  score ;
}STUDENT ;
void Info_input(STUDENT stu[], int n) ;
/* Function sort */

int main()
{
    int i, n;
    STUDENT stu[MAXSIZE] ;
    printf("Please enter the number of student:") ;
    scanf("%d", &n) ;
    Info_input(stu, n) ;             /* input student information */
	/* sort Function  */
    printf("* *After sort* *\n") ;
    for(i = 0; i < n; i++)
    {
        printf("%-10s%-4d\n", stu[i].name, stu[i].score) ;
    }
    return 0 ;
}
void Info_input(STUDENT stu[], int n)
/* input student information */
{
    int i ;
    printf("Please enter student information:\n") ;
    for(i = 0; i < n; i++)
    {
        scanf(" %s", stu[i].name) ;
        scanf("%d", &stu[i].score) ;
    }
}
/* Input */
WangGang 90		LiFang   96		NingYi   75		SunTan   96		ChenHao  72
Niudun   99		Zhaojin  60		Yangpeng 90		Oula     100	Ailun    86
Ouyang   90		Bifei    85		Ximen    96		Qihuan   80		Dugu     85
Yangyi   85		Wuyifan  95		LiMing   78		Kexi     99		Messi    100

2 排序算法

  正如前述,排序算法包括交换排序、选择排序等。在下面的讨论中,会涉及到基于学生成绩的实数型排序,还有基于学生姓名的字符串排序,学生成绩和姓名被称为关键字,本文介绍的大部分排序算法都是基于关键字比较的算法。在这里,对于一些常用的排序算法会给出相关的算法思想和C语言实现,而对于一些不常用的算法,如希尔排序等会给出相关的算法思想或伪代码表示,如不特别说明,这里介绍的算法都是升序算法。

2.1 交换排序

  交换排序是借鉴了最大值、最小值的思想,其基于关键字的比较来交换两个元素在序列中的位置,从而达到排序的目的。

2.1.1 冒泡排序(气泡排序)

  冒泡排序,也称为气泡排序,其从后往前依次比较相邻元素的大小从而将较小的元素移动到较前的位置,犹如一个气泡(较小值)慢慢浮出水面(放到最前面)。算法的核心思想如图所示:
第一趟冒泡排序
  排序的核心算法如下,在这里还利用了标记是否交换的变量flag(初始为0),在每趟比较时都会判断flag是否改变,如flag没有改变,那么说明本趟一次都没有交换过,也就是数据已然有序了。

void Bubble_sort(STUDENT stu[], int n)
/* Bubble sort */
{
    int i, j, temp, flag ;                       /* flag of the swap in sort*/
    char name[10] ;
    for(i = 0; i < n-1; i++)                     /* compare n-1 times       */
    {
        flag = 0 ;
        for(j = n-1; j > i; j--)                 /* nth comparison          */
        {
            if(stu[j].score < stu[j-1].score)    /* swap                    */
            {
                temp = stu[j].score ;
                strcpy(name, stu[j].name) ;
                stu[j].score = stu[j-1].score ;
                strcpy(stu[j].name, stu[j-1].name) ;
                stu[j-1].score = temp ;
                strcpy(stu[j-1].name, name) ;
                flag = 1 ;
            }
        }
        if(flag == 0)                            /* no swap means in order */
        {
            break ;
        }
    }
}

2.1.2 快速排序

  快速排序,简称快排,是冒泡排序的优化。冒泡排序每一趟都会比较相邻的两个元素,实际上有很多次都是冗余比较,而对于快速排序而言,快速排序是基于分治法的,快速排序不断地将所排序列分成两部分,每一部分都递归地求解。具体来说,快排有以下几个步骤:

  1. 选定一个基准元素(pivot),用该基准元素将序列二划分:小于等于pivot的一部分 + pivot +大于pivot的一部分,此时pivot在最终的排序位置;

  2. 然后递归地对小于等于pivot的一部分、大于pivot的一部分进行求解,终止条件为一个元素自然有序(即不断划分时,某一部分只有1个元素,此时该元素即在最终的位置)。

  算法的核心思想为如下图所示(一次划分),在这里pivot取第一个元素:

在这里插入图片描述
  算法的C语言实现有:

void Quick_sort(STUDENT stu[], int low, int high)
/* Quick sort */
{
    int pos ;                                 /* Middle Point */
    if(low < high)
    {
        pos = Partition(stu, low, high) ;
        Quick_sort(stu, low, pos-1) ;         /* First half   */
        Quick_sort(stu, pos+1, high) ;        /* Second half  */
    }
}
int Partition(STUDENT stu[], int low, int high)
/* Dividing the sequence into two parts */
{
    int pivot = stu[low].score ;
    char name[10] ;
    strcpy(name, stu[low].name) ;
    while(low < high)
    {
        while(low < high && stu[high].score > pivot)  /* Find <=pivot from right to left */
		{
			high -- ;
		}
        stu[low].score = stu[high].score ;            /* swap                            */
        strcpy(stu[low].name, stu[high].name) ;
        while(low < high && stu[low].score <= pivot)  /* Find >pivot from left to right  */
		{
			low ++ ;
		}
        stu[high].score = stu[low].score ;             /* swap                           */
        strcpy(stu[high].name, stu[low].name) ;
    }
    stu[low].score = pivot ;
    strcpy(stu[low].name, name) ;
    return low ;
}

2.1.3 交换排序小结

  冒泡排序在思想和实现上是简单的,但是它效率不高;快速排序借鉴了分治的思想,通过缩小处理的序列来求解问题:

时间复杂度最好情况平均情况下最坏情况备注
冒泡排序O(n)O(n2)O(n2)空间复杂度为O(1)
快速排序O(nlog2n)O(nlog2n)O(n2)平均情况下空间复杂度为O(log2n)

  其中,快速排序是递归算法需要利用递归栈,在最好和平均情况下空间复杂度为O(log2n),最坏情况下式O(n)。可以看出快速排序在平均情况下的时间复杂度和最好情况下的时间复杂度相当,因此快速排序算法是内部排序算法中平均性能最优的排序算法

2.2 选择排序

  选择排序是通过依次选出最小(最大)值来使序列有序,选择排序的关键在于如何选择这一过程,根据选择策略的不同可以将选择排序分为:简单选择排序和堆排序。

2.2.1 简单选择排序

  自然的想法是在序列中逐一选择最小(最大)的元素,然后确定它的位置,这是简单选择排序的思想:

  1. 首先选择最小值,将其放置在第一个位置;

  2. 再者选择剩余元素的最小值,将放置在第二个位置;

  可以发现,简单选择排序和冒泡排序十分类似,不过相对于简单选择排序,冒泡排序比较的次数相当,但移动次数较多,故简单选择排序每次选定最小值元素后再去移动,从而减少了移动的次数。
在这里插入图片描述
  其C语言算法实现有:

void Select_sort(STUDENT stu[], int n)
/* Select sort */
{
    int i, j, min_score, min_index ;             /* min_index is the index of min */
    char min_name[10] ;
    for(i = 0; i < n-1; i++)
    {
        min_score = stu[i].score ;
        min_index = i ;
        for(j = i+1; j < n; j++)
        {
            if(stu[j].score < min_score)          /* compare min value and j_value */
            {
                min_score = stu[j].score ;
                strcpy(min_name, stu[j].name) ;
                min_index = j ;
            }
        }
        stu[min_index].score = stu[i].score ;     /* swap min and i_th            */
        strcpy(stu[min_index].name, stu[i].name) ;
        stu[i].score = min_score ;
        strcpy(stu[i].name, min_name) ;
    }
}

2.2.2 堆排序

  了解堆排序前,首先要了解什么是堆,堆(heap)是一种完全二叉树,且父节点的元素大于(或小于)左右孩子,父节点的元素大于左右孩子的称为最大堆(Max heap),父节点的元素小于左右孩子称为最小堆(Min heap)。逻辑上,可以将堆看成下面图解的形式化描述:

在这里插入图片描述
  在此利用最小堆可以选出最小值,从而确定该元素的位置这也是堆排序的精髓:

  1. 将各元素建成一个最小堆,如将序列<2、5、7、6、9、8>建成一个堆(即上图最小堆,不唯一);

  2. 建成堆后,将堆顶(最小值)和堆的最末节点进行交换(上面最小堆的最末节点为8),并将该元素不考虑为堆的一部分;

  3. 将剩余的元素调整成新的堆,并逐一进行第2步操作,从而使序列有序。

  其流程图为:

在这里插入图片描述
在这里插入图片描述
  堆排序算法的效率是比简单选择排序高的,在上面的流程图中可以发现,对于有n个元素的序列,主要由两部分工作,第一部分就是初始建堆,第二是调整堆。初始建堆的过程是对n个元素进行比较的过程,时间复杂度为O(n),之后的调整堆的过程调整次数不超过堆的高度,时间复杂度为O(log2n) ,故总的实际复杂度为O(nlog2n) 。

  堆排序在某些情况下是十分适合使用的,比如在100亿个数中选出最小的前100个数就可以使用堆:

  1. 从100亿个数中取出100个数,并将其建成最大堆;

  2. 将100亿个数中剩余的数和堆顶元素(最大值)比较,若大于堆顶元素,则丢弃该数;若小于堆顶元素,则将该元素和堆顶互换,然后整理成新的堆;

  3. 依次进行第2步操作,直至所有数都比较完,堆中所剩的元素就是前100个最小值。

2.2.3 选择排序小结

  选择排序的思想是十分清晰的,其中简单选择排序实现简单,而堆排序作为简单选择排序的优化,通过利用堆这种数据结构来提高算法的效率。实际上,数据结构的复杂在一定程度上是可以提高算法效率的,在《数据结构》课程中或许会有更深的体会。

时间复杂度最好情况平均情况下最坏情况备注
简单选择排序O(n2)O(n2)O(n2)空间复杂度为O(1)
堆排序O(nlog2n)O(nlog2n)O(nlog2n)空间复杂度为O(1)

  值得注意的是,堆排序虽然是一种比较复杂的数据结构,但是可以用数组存储并且可以原地工作(不需要额外的存储空间),故空间复杂度为O(1)。

2.3 插入排序

  插入排序在日常生活中是十分常见的,比如打扑克牌时,整理牌的过程就是插入排序的过程。下面介绍的三种排序算法都是基于此的。

2.3.1 直接插入排序

  直接插入排序就是整理扑克牌的过程,其基本思想是:对于已经有序的序列中,插入一个元素,则可以将该元素 x 和有序序列中的元素 <S1、S2、…、Sm> 进行比较,从Sm开始比较找到第一个不大于元素 x 的 Si ,并将x插入到Si的后面,此时有序序列长度增加1变为:<S1、S2、…、Si、x、Si+1…、Sm> ,其过程图下所示:
在这里插入图片描述
  直接插入排序的C语言实现是简单的:

void Insert_sort(STUDENT stu[], int n)
/* Insert sort */
{
    int i, j, temp ;
    char name[10] ;
    for(i = 0; i < n-1; i++)                     /* <0, 1, ..., i> is sorted */
    {
        for(j = i+1; j > 0; j--)                 /* Insert i+1 */
        {
            if(stu[j].score < stu[j-1].score )   /* swap j and j-1 */
            {
                temp = stu[j-1].score ;
                strcpy(name, stu[j-1].name) ;
                stu[j-1].score = stu[j].score ;
                strcpy(stu[j-1].name, stu[j].name) ;
                stu[j].score = temp ;
                strcpy(stu[j].name, name) ;
            }
        }
    }
}

2.3.2 折半插入排序

  折半插入排序是对直接插入排序的改进,在直接插入排序中找到插入的位置是关键的,而折半插入排序利用将该元素 x 和有序序列中的元素 <S1、S2、…、Sm> 过程中, <S1、S2、…、Sm> 已经有序的特点,可以利用折半查找的方法去实现(在后面的讲述中会知道对于有序序列而言,折半查找在效率上优于顺序查找),通过折半查找便可以在O(log2m)的时间内找到插入位置(而之前需要O(m))。

  折半插入排序的基本算法是:

void Bininsert_sort(STUDENT stu[], int n)
/* Insert sort */
{
    int i, j, temp, low, high, mid ;
    char name[10] ;
    for(i = 0; i < n-1; i++)                     /* <0, 1, ..., i> is sorted */
    {
        low  = 0, high = i ;
        while(low <= high)                       /* search insert position */
        {
            mid = (low + high)/2 ;
            if(stu[mid].score <= stu[i+1].score)
            {
                low = mid + 1 ;
            }
            else
            {
                high = mid - 1 ;
            }
        }
        temp = stu[i+1].score ;
        strcpy(name, stu[i+1].name) ;
        for(j = i; j >= low; j--)                 /* Insert i+1 */
        {
            stu[j+1].score = stu[j].score ;
            strcpy(stu[j+1].name, stu[j].name) ;
        }
        stu[low].score = temp ;
        strcpy(stu[low].name, name) ;
    }
}

2.3.3 希尔排序

  希尔排序也是一种插入排序,其基于直接插入排序算法改进而来,又称为缩小增量排序。算法的思想是清晰的,下面有图示给出:
在这里插入图片描述
  希尔排序关键在于增量d的选择,一般而言初始时增量d会选择为数据量(n)的一半,然后逐渐减半,直至d等于1。之所以希尔排序的效率较高是因为当增量d逐渐减小时,数据量已经基本有序,对于大致有序的数据而言直接插入排序可以达到O(n)的时间复杂度,希尔排序正是利用了这一点。

2.3.4 插入排序小结

  插入排序的思想都是简单的,在生活中的扑克牌游戏的整理牌的过程就由此而来,对于三种插入排序而言,他们的时空效率有:

时间复杂度最好情况平均情况下最坏情况备注
直接插入排序O(n)O(n2)O(n2)空间复杂度为O(1)
折半插入排序O(n2)O(n2)O(n2)空间复杂度为O(1)
希尔排序O(n1.3)O(nlog2n)O(n2)空间复杂度为O(1)

  值得注意的是,希尔排序的时间复杂度和增量d的选择有关,不同的选择时间复杂度不一样,由于这涉及到数学上未解决的问题,所以在一定情况下其时间复杂度在O(n1.3)和O(n2)之间。

2.4 归并排序

  归并排序是将序列划分成若干个元素数相同的子序列后,依次将多个有序的子序列合并为一个子序列的过程,在这里将介绍一下二路归并排序,也即将依次将两个有序子序列合并为一个子序列的过程,其基本思想如图所示:

在这里插入图片描述
  归并排序的精髓在于子序列归并的过程,由于两个子序列都有序,因此可以通过双指针法访问两个子序列来合并成一个子序列。其C语言实现有:

void Merge_sort(STUDENT stu[], int low, int high)
/* Merge sort */
{
    int mid ;
    if(low < high)
    {
        mid = (low + high)/2 ;             /* Divide Subsequences    */
        Merge_sort(stu, low, mid) ;        /* Left subsequence       */
        Merge_sort(stu, mid+1, high) ;     /* Right subsequence      */
        Merge(stu, low, mid, high) ;       /* Merge two subsequences */
    }
}
void Merge(STUDENT stu[], int low, int mid, int high)
/* Merge two subsequences */
{
    STUDENT *temp ;
    int n = high - low + 1 ;
    int i, j, k ;
    temp = (STUDENT*)malloc(n * sizeof(STUDENT)) ;
    i = low, j = mid + 1, k = 0 ;
    while(i <= mid && j <= high)                   /* merge, write min to temp */
    {
        if(stu[i].score < stu[j].score)
        {
            temp[k].score = stu[i].score ;
            strcpy(temp[k].name, stu[i].name) ;
            i ++, k ++ ;
        }
        else
        {
            temp[k].score = stu[j].score ;
            strcpy(temp[k].name, stu[j].name) ;
            j ++, k ++ ;
        }
    }
    while(i <= mid)                                /* Remaining elements        */
    {
        temp[k].score = stu[i].score ;
        strcpy(temp[k].name, stu[i].name) ;
        i ++, k ++ ;
    }
    while(j <= high)                               /* Remaining elements        */
    {
        temp[k].score = stu[j].score ;
        strcpy(temp[k].name, stu[j].name) ;
        j ++, k ++ ;
    }
    for(i = low, j = 0; i<=high && j<k; i++,j++)   /* write back to stu[]       */
    {
        stu[i].score = temp[j].score ;
        strcpy(stu[i].name, temp[j].name) ;
    }
}

  在分解和合并的过程中,函数void Merge_sort(STUDENT stu[], int low, int high)是比较难理解的,该函数是一个递归的函数,现将序列划分为两个子序列直至该子序列只有一个元素(一个元素的序列是自然有序的),然后返回的有序序列,之后的函数void Merge(STUDENT stu[], int low, int mid, int high)则是将两个子序列合并为一个子序列,从而实现归并排序。其时空效率为

时间复杂度最好情况平均情况下最坏情况备注
二路归并排序O(nlog2n)O(nlog2n)O(nlog2n)空间复杂度为O(n)

  二路归并排序可以扩展成多路归并排序,多路归并排序是外部排序常常采用的算法,其时间效率是非常高的,不过归并排序的主要问题在于归并排序的空间效率上,其需要一个辅助数组来存储归并后的数据,因此空间复杂度为O(n)。

2.5 基数排序

  上面介绍的排序方法都是基于比较的排序方法,基于比较的排序算法的时间复杂度的下界是O(nlog2n),为了突破这一下界,提出了一些不基于比较的算法,本节讲述的基数排序就是如此。

  基数排序是基于关键字分量的大小来排序的,其主要思想是:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。基数排序的方式可以采用LSD(Least significant digital)MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

  基数排序的定义是抽象的,下面用图解来解释:假设待排序的序列为:<269, 375, 103, 008, 075, 810, 576, 630>,其有三个关键字分量:<个位,十位,百位>,排序的顺序是个位 → 十位 → 百位,因为十位、百位的优先级高,一个数字的百位比另一个数字的百位大,那么前者就大于后者。

在这里插入图片描述
  这里讨论一下具体的实现,其主要由两个步骤组成:

  1. 分配
    将数字按照排序的关键字分配成n个队列(可以理解为存放数字的容器),队列中的数字的关键字相同;
  2. 收集
    将所有数字按照关键字的大小依次手机并整理成一个序列,此时得到的是相对于该关键字的有序序列

  下面用图解表示,将<269, 375, 103, 008, 075, 810, 576, 630>按照个位的大小来分配
在这里插入图片描述

  之后再根据十位百位数字来对序列进行排序,最后实现对于整个序列的排序。在这个排序过程中,没有对于各个关键字进行比较,现分析时间复杂度和空间复杂度:

  假设序列中有n个关键字,关键字的分量为d(比如3位整数有个位、十位和百位3个分量),每个分量的基数为r(比如个位的基数为10,因为可取0 ~ 9之间的整数),因此在上面的分配阶段需要O(n)的时间,在收集阶段需要O(r)的时间,总共有d次分配和收集,故总时间复杂度为O(d(n+r))。而空间上,需要r个队列来存储分配的数据,这里的队列是一种数据结构,可以简单地认为是生活中的排队队列,故空间复杂度为O(r)

时间复杂度最好情况平均情况下最坏情况备注
基数排序O(d(n+r))O(d(n+r))O(d(n+r))空间复杂度为O(r)

  可以看到,其时间复杂度为O(d(n+r))是线性的,而且突破了基于比较的排序算法的时间复杂度下界,不过对于基数排序也有一定的局限性,主要在于基数排序主要适合于可以利用关键字排序的情况,比如整数。不过对于浮点数之类的情况,可以通过一些转换,比如同时将小数点移位后比较整数部分等方法。总而言之,基数排序不仅仅是提供了一种排序方法,更是一种排序思想。

2.6 排序算法总结

  对于上述介绍的各种排序算法,总的来说在考虑时间复杂度和空间复杂度后,同时结合程序的要求可以选择不同的排序算法。

  对于较小的数据量,冒泡排序、简单选择排序、直接插入排序这类简单的排序算法效率不错。对于较大的数据量,选择一些复杂算法,如快速排序、堆排序等算法是被优先考虑的。不过根据大量的文献介绍,快速排序一直是一种值得推荐的排序方法。

  另外结合不同的排序方法在有些时候有意想不到的结果,比如结合快速排序和基数排序提出了超快速排序算法1等,这种结合的思想是值得学习的。

时间复杂度最好情况平均情况下最坏情况备注
冒泡排序O(n)O(n2)O(n2)空间复杂度为O(1)
快速排序O(nlog2n)O(nlog2n)O(n2)平均情况下空间复杂度为O(log2n)
简单选择排序O(n2)O(n2)O(n2)空间复杂度为O(1)
堆排序O(nlog2n)O(nlog2n)O(nlog2n)空间复杂度为O(1)
直接插入排序O(n)O(n2)O(n2)空间复杂度为O(1)
折半插入排序O(n2)O(n2)O(n2)空间复杂度为O(1)
希尔排序O(n1.3)O(nlog2n)O(n2)空间复杂度为O(1)
二路归并排序O(nlog2n)O(nlog2n)O(nlog2n)空间复杂度为O(n)
基数排序O(d(n+r))O(d(n+r))O(d(n+r))空间复杂度为O( r )

2.7 补充知识:物理排序和索引排序

  物理排序是指通过移动元素在实际物理存储空间中的存放位置而实现的排序,索引排序是指通过移动元素的索引地址而实现的排序。

  或许在之前的程序中就会发现,在实现排序时的算法中交换两个学生分数时同时交换了两个学生的姓名,这是耗费时间的。如果在一个结构体中,存储的其他信息过多,那么实现交换的代价也越大,因此不建议使用整体的交换,而推荐使用索引地址的交换。下面通过指针数组的方式来实现示例结构中将学生姓名按照字典顺序排序。

...
STUDENT *ptr[MAXSIZE] ;             /* pointer array */
...
for(i = 0; i < n; i ++)
{
    ptr[i] = &(stu[i]) ;
}
...
void Dir_sort(STUDENT*p[], int n)
/* Sort in dictionary order by name */
{
    int i, j, min ;
    STUDENT *temp ;
    for(i = 0; i < n-1; i++)        /* select sort */
    {
        min = i ;
        for(j = i+1; j < n ;j++ )
        {
            if(strcmp(p[j]->name, p[min]->name) < 0)
            {
                min = j ;
            }
        }
        temp = p[i] ;              /* swap diress */
        p[i] = p[min] ;
        p[min] = temp ;
    }
}

  在这里,利用一个指针数组STUDENT *ptr[MAXSIZE] ;来保存STUDENT类型数据的地址,然后通过交换地址的方法来交换指向数据的索引,从而大大排序的目的,在这个过程中,避免了改变元素的实际存储位置,从而使得算法更加高效。

3. 查找算法

  在序列中搜索一个特定元素的处理过程,称为查找(searching)。对于查找算法的效率问题更多的不是考虑查找的时间和空间复杂度,而是平均查找长度。查找过程中,一次查找的长度是指需要比较的关键字次数,平均查找长度即指所有查找过程中进行的关键字的比较次数的平均值,具体含义在下面的例子中阐明。另外,在下面查找实例中是通过输入学生的姓名,若找到则输出学生的成绩,若没有找到则提示信息。

3.1 顺序查找算法

  当生活中查找通讯录的名单时,最简单的办法是从上至下看完整个通讯录然后定位通讯录中所想查找的电话号码。本节将介绍的顺序查找也是如此,顺序查找算法通过顺序扫描整个序列,依次将每个元素与待查找值进行比较;若找到则输出位置值,若遍历完整个序列都未找到,则结束查找输出未找到的信息,其基本的算法是:

int Seq_search(STUDENT stu[], int n, char name[10])
/* sequential search */
{
    int i = 0 ;
    while(i < n && strcmp(stu[i].name, name) != 0)      /* search       */
    {
        i ++ ;
    }
    if(i < n)                                           /* found        */
    {
        return i ;                                      /* return index */
    }
    else                                                /* not found    */
    {
        printf("Not found!") ;
        return n ;
    }
}

  算法是简单的,但是可能任意一个程序设计人员都知道这不是一个高效的算法,现在用平均查找长度来衡量一下:
在这里插入图片描述
  对于上面的序列,假设任意一个元素被查找的概率相同,那么其查找成功和查找失败的平均查找长度为:
在这里插入图片描述
  查找失败的平均查找长度要比较n个元素还有最后的一个空元素(即这里认为查找到空元素时才认为查找失败)。可以看出查找成功和查找失败的平均查找长度都是n的线性倍数的。

3.2 折半查找算法

  正如前面s所提及的那样,顺序查找效率不高,因为一遍都需要遍历序列中的一半的元素,那么下面介绍的折半查找算法是一种高效率的算法,不要折半查找算法要求序列是有序的。其基本思想是:针对有序序列,先用待查找元素和序列的中间元素进行比较,比较完之后判断待查找元素可能在原序列上/下半部分,然后在上/下半部分极继续执行上面的操作,直至找到元素或未找到元素。
在这里插入图片描述
  下面是C语言实现:

int Bin_search(STUDENT stu[], int n, char name[10])
/* binary search */
{
    int low = 0, high = n-1, mid ;
    while(low <= high)
    {
        mid = low + (high - low)/2 ;
        if(strcmp(stu[mid].name, name) == 0)     /* found            */
        {
            return mid ;
        }
        else if(strcmp(stu[mid].name, name) < 0) /* right subseqence */
        {
            low = mid + 1 ;
        }
        else                                     /* left subseqence  */
        {
            high = mid - 1 ;
        }
    }
    return n ;
}

  折半查找的查找成功和查找失败的平均查找长度是不容易求解的,一般需要写出折半查找的判定树之后进行求解,在此不再赘述。

3.3 分块查找算法

  分块查找结合了顺序查找和折半查找的优点,既有动态结构,又适于快速查找。分块查找的基本思想是:将查找表分为若干个子块。块内元素是无序的,而块间元素是有序的其中会建立索引,取每块的最大元素建立索引表。分块查找的过程分为两步:

  1. 在索引表中确定待查记录所在的块,可以通过顺序查找和折半查找索引表;

  2. 在块内顺序查找。

在这里插入图片描述
  下面分析分块查找的效率,对于长度为n的查找表,假设均分成b块,每块有s个元素,在等概率的情况下,若在块内和索引表总均采用顺序查找,则平均查找长度为(查找成功):
在这里插入图片描述

3.4 查找算法总结

  顺序查找、折半查找和分块查找方式都是常用的查找方式,值得注意的是,在一些存储连续的序列中(如链表)只能采取顺序查找的方式,而对于数组等存储方式是可以采取折半查找的方法的。

  当然还有一些其他的查找算法,如B-树查找、散列查找等,这些查找算法都是建立了一定的数据结构从而来查找的,因此在讨论算法是是不可以忽视采用的数据结构的。


  1. 周建钦. 超快速排序算法[J]. 计算机工程与应用, 2006. ↩︎

  • 13
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Stu_Yang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值