【数据结构】九、排序

目录

一、排序概述

二、插入排序

2.1直接插入排序

2.2折半插入排序

2.3二路插入排序

2.4表插入排序

2.5希尔排序

三、交换排序

3.1冒泡排序

3.2快速排序

四、选择排序

4.1简单选择排序

4.2锦标赛排序

4.3堆排序

五、归并排序

六、基数排序

七、总结 


一、排序概述

定义:将一组存放在数据表中的无序数据按照一定的顺序排列起来

目的:便于查找

排序算法衡量指标

  • 时间效率——排序速度
  • 空间效率——占内存辅助空间大小
  • 稳定性——若两个记录A和B的关键字值相等,且排序后A、B的先后次序保持不变,则称这种排序算法是稳定的

内部排序:待排序记录都在内存中

外部排序:数据量过大,将数据分批调入内存进行排序,还要将结果及时放入外存

待排序记录在内存中的存储和处理

  1. 顺序排序:排序时直接移动记录
  2. 链表排序:排序时只移动指针
  3. 地址排序:排序时先移动地址,最后再移动记录 

内部排序算法分类

  1. 插入排序
  2. 交换排序
  3. 选择排序
  4. 归并排序
  5. 基数排序 

列表结构及建立

typedef struct {
	int array[MAXSIZE + 1];
	int length;
}list;

void initlist(list& l)
{
	printf("number?");
	scanf_s("%d", &l.length);
	for (int i = 1; i <= l.length; i++)
	{
		printf("data:");
		scanf_s("%d", &l.array[i]);
	}
}

二、插入排序

基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。简言之,边插入边排序,保证子序列中随时都是排好序的。

2.1直接插入排序

对于每一个元素,在已形成的有序线性表中从后向前找插入位置,原位置元素向后移,新元素插入。是最简单的排序法。

直接插入排序
时间效率

最好:O(n),数据本就有序,比较n-1次

最差:O(n^2),数据逆序,从第二个元素开始,每个元素都要与有序部分全比较一遍,比较1+2+...n-1次

空间效率

O(1),占用一个缓冲单元
稳定性 稳定

代码

#include
#include

#define MAXSIZE 20

typedef struct {
	int array[MAXSIZE + 1];
	int length;
}list;

void initlist(list& l)
{
	printf("number?");
	scanf_s("%d", &l.length);
	for (int i = 1; i <= l.length; i++)
	{
		printf("data:");
		scanf_s("%d", &l.array[i]);
	}
}

void sort(list& l)
{
	int i, j;
	for (i = 2; i <= l.length; i++)  //0号为缓冲位,1号默认为有序,所以从2号开始遍历
	{
		if (l.array[i - 1] > l.array[i])    //新元素大于等于有序部分最后一位(即前一个元素)时无需操作,小于时进行操作
		{
			l.array[0] = l.array[i];        //新元素值复制到缓冲区
			j = i-1;
			while (l.array[j] > l.array[0]) //将有序部分大于新元素的部分后移
			{
				l.array[j + 1] = l.array[j];
				j--;
			}
			l.array[j + 1] = l.array[0];	//新元素插入
		}
	}

	for (i = 1; i <= l.length; i++)
		printf("%d ", l.array[i]);
}

int main() {
	list l;
	initlist(l);
	sort(l);
}

2.2折半插入排序

子表有序且为顺序存储结构,则插入时采用折半查找定可加速定位。与直接插入排序相比,元素间的比较次数减少

折半插入排序
优点 比较次数大大减少,全部元素比较次数仅为O(nlog_2 n)
时间效率 移动次数并未减少, 所以排序效率仍为O(n^2)
空间效率 O(1)
稳定性 稳定

2.3二路插入排序

增加一个元素数和待排数组相同的循环数组d

先将list第一个元素赋给d第一个元素,然后以此元素为基准,把list中其他元素插入到基准的前面或后面,最终从头指针到尾指针即为有序数组

2.4表插入排序

在顺序存储结构中,给每个记录增开一个指针分量,在排序过程中不移动元素,只修改指针,最终得到一个有序链表

【数据结构】九、排序_第1张图片 表插入排序举例
表插入排序
时间效率 无需移动记录,只需修改2n次指针值。但由于比较次数没有减少,故时间效率仍为O(n^2)
空间效率 低,因为增开了指针分量。但在运算过程中没有用到更多的辅助单元
稳定性 稳定

2.5希尔排序

先将整个待排序列分割为若干子序列,子序列中分别插入排序,再扩大子序列范围,插入排序,至整个序列有序

子序列的选择:将相隔一定距离的元素划分为一个序列,间隔逐渐缩短

希尔排序
时间效率 O(n^1.25)~O(1.6n^1.25)      由经验公式得到
空间效率 O(1)
稳定性 不稳定

代码

#include 
#include 

void shellsort(int* a, int len)
{
    int i, j, k, tmp, gap;  // gap 为步长
    for (gap = len / 2; gap > 0; gap /= 2) // 步长初始化为数组长度的一半,每次遍历后步长减半
    { 
        for (i = 0; i < gap; ++i) // 变量 i 为每次分组的第一个元素下标;间隔数也是组数
        {  
            for (j = i + gap; j < len; j += gap) //对每组元素进行插入排序;在每组中第一个默认为有序,从第二个开始遍历
            { 
                tmp = a[j];  // 备份a[j]的值
                k = j - gap;  // k初始化为j的前一个元素

                while (k >= 0 && a[k] > tmp) // 把一组中有序部分比tmp的值大的元素向后移动
                {
                    a[k + gap] = a[k]; 
                    k -= gap;
                }
                a[k + gap] = tmp;   // 把tmp插入
            }
        
        }
    }
}

int main(void)
{
    int i, len, * a;
    printf("请输入要排的数的个数:");
    scanf_s("%d", &len);
    a = (int*)malloc(len * sizeof(int)); // 动态定义数组
    if (!a)  return NULL;

    printf("请输入要排的数:\n");
    for (i = 0; i < len; i++) // 数组值的输入
        scanf_s("%d", &a[i]);

    shellsort(a, len); // 调用希尔排序函数

    printf("希尔升序排列后结果为:\n");
    for (i = 0; i < len; i++) // 排序后的结果的输出
        printf("%d\t", a[i]);
    printf("\n");
}

用希尔排序方法对一个数据序列进行排序时,若第1趟排序结果为9,1,4,13,7,8,20,23,15,则该趟排序采用的增量(间隔)可能是(   )

  • A. 2
  • B. 3
  • C. 4
  • D. 5

 答案:B    依次对选项进行假设,当组内都有序时假设成立 

三、交换排序

两两比较关键码,如果逆序,则进行交换

3.1冒泡排序

每趟不断将相邻两记录两两比较,并按“前小后大”(或“前大后小”)规则交换。每趟结束时,能挤出一个最大值到最后面

冒泡排序
优点 每一趟整理元素时,不仅可以完全确定表尾一个元素的位置,还可以对前面的元素作一些整理,所以比一般的排序要快

时间效率

最好情况:初始排列有序,只执行一趟冒泡,做 n-1 次比较,不移动对象;

最坏情形:初始排列逆序,执行n-1趟冒泡,第i趟(1<=i<=n) 做了n - i 次比较,执行了 n - i 次交换.此时比较总次数:n(n-1)/2  记录移动总次数:3/2n(n-1)

最坏情况O(n^2)

空间效率

O(1)
稳定性 稳定

代码

i的范围:0~len-1

j的范围:1~len-i

flag的作用:发生交换flag才会变为1,内层循环结束后flag为0说明未发生交换,数组已有序,可以结束,避免无效操作

#include
#include

void sort(int* list, int len)
{
    int flag = 1;
    for (int i = 0; i < len - 1 && flag == 1; i++)  //进行len-1次排序
    {
        flag = 0;
        for (int j = 1; j < len - i; j++)   //每次从第2个元素遍历到len-i号元素
        {
            if (list[j - 1] > list[j])
            {
                int t = list[j];
                list[j] = list[j - 1];
                list[j - 1] = t;
                flag = 1; //flag的作用:发生交换flag才变为1;第二次循环结束没有发生交换说明该序列已有序,可以结束
            }
        }
    }
}

int main() {
	int i, len, * a;
    printf("请输入要排的数的个数:");
    scanf_s("%d", &len);
    a = (int*)malloc(len * sizeof(int)); // 动态定义数组
    if (!a)  return NULL;

    printf("请输入要排的数:\n");
    for (i = 0; i < len; i++) // 数组值的输入
        scanf_s("%d", &a[i]);

    sort(a, len);

    for (i = 0; i < len; i++)
        printf("%d ", a[i]);
}

对一组数据(2,12,16,88,5,10)进行排序,若前三趟排序结果如下( )
第一趟:2,12,16,5,10,88
第二趟:2,12,5,10,16,88
第三趟:2,5,10,12,16,88
则采用的排序方法可能是:

  • A. 快速排序
  • B. 冒泡排序
  • C. 基数排序
  • D. 希尔排序

答案:B   每次排序都有最大值挤到最后 

3.2快速排序

思路以首元素为基准,首元素此时为“空闲位”。先从后向前找比首元素小的,复制到空闲位,自身变为新的空闲位;再从前往后找比基准大的,复制到空闲位,自身成为新的空闲位。当前后指针相遇时把基准插入。此时列表基准左边都比它小,右边都比它大,向左右递归继续排序。

时间效率

最好情况,每次的基准数都能平分数组,每趟可以确定的数据元素是2的次方,总共logn趟,每趟涉及n个元素,时间复杂度为O(nlog_2n)

最坏情况,每次的基准数都是最大/最小值,每次只能确定一个数,相当于冒泡,时间复杂度为O(n^2)

我们认为快速排序平均时间复杂度为  O(nlog_2n)  

空间效率 O(log_2n)     当哨兵不是最大/最小值的情况时,递归要用栈(存每层low,high和pivot),共需要logn层
稳定性 不稳定

代码

//快速排序:以首位数为基准,用两指针从两侧逼近,把小于基准的都放到基准左侧,大于基准的都放到基准右侧,再缩小范围,递归。
void quicksort(int*& list, int low, int high)  //传入数组,开始和结束的编号
{ 
	if (low < high) 
	{
		int p1 = low;
		int p2 = high;       //两指针指向两侧
		int target = list[low];   //首元素为基准

		while (p1 < p2)  //以下操作都需要当两指针未重合时才能执行
		{ 
			while (p1 < p2 && list[p2] > target)  //从后向前找到不大于target的值
				p2--;

			if (p1 < p2)
				list[p1++] = list[p2];    //把这个值赋给p1,p1后移

			while (p1 < p2 && list[p1] < target)    //从前向后找到不小于target的值
				p1++;

			if (p1 < p2)
				list[p2--] = list[p1];    //把这个值赋给p2,p2前移
		}
		list[p1] = target;     //把重合之处赋为target

		//递归调用
		quicksort(list, low, p1 - 1);
		quicksort(list, p1 + 1, high);
	}
}

下列序列中,(    )是执行第一趟快速排序后所得的序列。(下划线表示已找到位置的元素)

  • A. [68,11,18,69]_[23,93,73]
  • B. [68,11,69,23]_[18,93,73]
  • C. [93,73]_[68,11,69,23,18]
  • D. [73,11,69,23,18]_[93,68]

答案:D  只有D能找到一个数,使左边数组都小于它,右边数组大于它

四、选择排序

选择排序的基本思想:每一趟(第i趟)在后面n - i + 1个待排记录中选取关键字最小的记录作为有序序列中的第 i 个记录。

4.1简单选择排序

简单选择排序
时间效率 O(n^2)   走 n - 1 趟,每趟遍历 n - i 个元素
空间效率 O(1)
稳定性 不稳定

代码

void simple_section_sort(int* list, int len)
{
    int min, x;
    for (int i = 0; i < len - 1; i++)  // 要对前n-1个位置进行选择,插入
    {
        min = 65535;
        x = i;
        for (int j = i; j < len; j++)  //选择出i到末尾的最小值
        {
            if (list[j] < min)
            {
                min = list[j];
                x = j;
            }
        }           
        int tmp = list[x];   //将最小值与第i个元素交换
        list[x] = list[i];
        list[i] = tmp;

    } 
}

对一组数据(84,47,25,15,21)排序,数据的排列次序在排序的过程中的变化为
(1) 84 47 25 15 21  (2) 15 47 25 84 21   (3) 15 21 25 84 47  (4) 15 21 25 47 84 
则采用的排序是 (     )

  • A. 选择
  • B. 冒泡
  • C. 快速
  • D. 插入

答案:A     最小值不断增加

4.2锦标赛排序

思路:把待排元素当作一棵完全二叉树的叶子节点,叶子不足的补齐;从下向上建立二叉树,父节点为子节点中较小值;此时根节点为排序的第一个结果;然后把树中的这个值变为无穷,更新父节点,得到新的根节点为排序的第二个结果;直到所有叶节点都遍历。

锦标赛排序
时间效率 O(nlog_2n)   n个记录各自比较约log2n次
空间效率 O(n)    胜者树附加节点n-1个
稳定性 稳定    当遇到两个数值相同时,认为左边较小

代码

不写了,知道过程就行  :)

4.3堆排序

准备知识

1.堆的结构是完全二叉树。构造堆的时候从上往下,从左往右依次添加节点

2.一般用数组来表示堆,用下标的计算关系来指明父子关系

3.下标为 i 的结点的父结点下标为 (i-1)/2;其左右子结点分别为 (2i + 1)、(2i + 2)

4.大根堆:父节点值大于两子节点     小根堆:父节点值小于两子节点

建堆

例:关键字序列T= (21,25,49,25,16,08),建大根堆

1.将原始序列画成完全二叉树的形式(准备知识1),第一个节点编号为1

【数据结构】九、排序_第2张图片 初始堆

2.从完全二叉树的最后一个非终端节点(编号为 [n/2])开始调整,图中为49

3.该节点(49)与它的两个子节点(08)进行比较,把较大值放到父节点的位置(49>8,不动)

4.对倒数第二个非终端节点进行此操作(25>25*,25>16,不动 ),一直遍历到根节点

5.另外,被换下来的节点要继续向下比较(如根节点21,21<25<49,21与49交换,交换后21要与子节点(08)继续比较,判断是否还要交换)

【数据结构】九、排序_第3张图片 大根堆

此时根节点为最大值,开始排序

排序

1)最大值在根节点,也就是数组第一位。把第一位与最后一位第n位交换,把最大值放到最后

2)数组从1到n-1重新建堆。最大值又上浮到第一位

3)再对调第一位和第n-1位,如此重复,得到有序序列

算法流程

void HeapSort (HeapType &H )  //对顺序表H进行堆排序
{
    for (i = H.length / 2; i >0; --i)
       HeapAdjust(H, i, H.length);     //倒序遍历非叶子节点,建立初始堆

    for (i = H.length; i > 1; --i) 
    { 
       H.r[1] ←→ H.r[i];         //最大值和尾部交换
       HeapAdjust(H, 1, i-1);    //把刚换上去的头节点进行调整,重建最大堆
    } 
}

//HeapAdjust是针对结点 i 的堆调整函数,其含义是:从结点i开始到堆尾为止
//从上向下比较,如果子女的值大于双亲结点的值,则互相交换,即把局部调整为大根堆
堆排序
时间效率 O(nlog_2n)   因为整个排序过程中需要调用n-1次HeapAdjust( )算法,而算法本身耗时为log_2n
空间效率 O(1)     仅在首尾交换时用到一个临时变量temp
稳定性 不稳定

代码

void heapadjust(int a[], int i, int m)
{
	int current = i;     //标记当前的根节点,调整一次过后还要继续向下比较
	int temp = a[i];     //根节点的值
	int child = i * 2;   //左孩子

	while (child <= m)
	{
		if (child < m && a[child] < a[child + 1])  //找较大的孩子
			child++;

		if (temp >= a[child])     //如果根不小于较大孩子,直接退出
			break;
		else
		{
			a[current] = a[child];//较大孩子上移,更新当前根节点
			current = child;
			child *= 2;
		}
	}
	a[current] = temp;   //最后赋值
}

void heapsort(int a[], int num)	//排序 
{
	int i, temp;
	for (i = num / 2; i > 0; i--)	//从最后一个父母节点开始,直到根节点 
	{
		heapadjust(a, i, num);
	}
	for (i = num; i > 1; i--)	//删除根节点后,重新构造堆 
	{
		temp = a[1];            //第一个节点和尾部节点交换,重新建堆
		a[1] = a[i];
		a[i] = temp;
		heapadjust(a, 1, i - 1);
	}
}

练习

已知关键序列5,8,12,19,28,20,15,22是小根堆(最小堆),插入关键字3,调整后得到的小根堆是?

答案:3,5,12,8,28,20,15,22,19

五、归并排序

思路:把一个长度为n的无序序列看成是n个长度为 1 的有序子序列。将数组的每一个元素划分为一个有序表。两两合并,组内分别排序;再两两合并,组内排序;最终得到有序序列。

【数据结构】九、排序_第4张图片 归并排序示意图
归并排序
时间效率 O(nlog_2n)     有序子序列的长度是以2的次方递增的,所以归并过程有log_2n“层”;每层要归并 n / len 次,每次要比较 len 次
空间效率 O(n)      在合并两子序列时,需要一个长度为n的辅助序列
稳定性 稳定

代码

#include
#include
 
void merge(int* list, int low, int mid, int high)
{
    int p;
    int* a = (int*)malloc(sizeof(int) * (high - low + 1));
    if (!a)  return;

    for (p = low; p <= high; p++)
        a[p] = list[p];

    int i, j, k;
    for (i = low, j = mid + 1, k = low; i <= mid && j <= high; k++)
    {
        if (list[i] > list[j])
            a[k] = list[j++];
        else
            a[k] = list[i++];
    }

    while (i <= mid)
        a[k++] = list[i++];
    while (j <= high)
        a[k++] = list[j++];

    for (p = low; p <= high; p++)
        list[p] = a[p];
}

void mergesort(int* list, int low, int high)  //归并排序
{
    if (low < high)   
    {
        int mid = (low + high) / 2;
        mergesort(list, low, mid);         //对前半部分归并排序
        mergesort(list, mid + 1, high);    //对后半部分归并排序
        merge(list, low, mid, high);       //把排序完的部分合并
    }
}

int main(void)
{
    int i, len, * list;
    printf("请输入要排的数的个数:");
    scanf_s("%d", &len);
    list = (int*)malloc(len * sizeof(int)); // 动态定义数组
    if (!list)  return NULL;

    printf("请输入要排的数:\n");
    for (i = 0; i < len; i++) // 数组值的输入
        scanf_s("%d", &list[i]);

    mergesort(list, 0, len - 1);

    for (i = 0; i < len; i++)
    printf("%d ", list[i]);

}

六、基数排序

基本思想:从多维度对元素进行排序,比如对扑克牌进行排序,可以先按花色分类,再按数字大小分类;也可以先按数字大小分类,再按花色分类。这也叫多关键字排序。

多关键字排序的实现通常有两种实现方法:

  • 最高位优先法MSD (Most Significant Digit first)
  • 最低位优先法LSD (Least Significant Digit first)

若规定花色为第一关键字(高位),面值为第二关键字(低位),则上文第一种方法为MSD,第二种为LSD。

数值排序思路

把数字的每一位看作一个维度。最多有几位,就要经过几趟排序。先从数字最低位开始比较。

【数据结构】九、排序_第5张图片

建立编号0~9的十个队列

遍历数组中的数字,按照个位数字将它们放入对应的队列(分配)

遍历完后,按照从上到下,从左到右的顺序把数字从队列中取出得到第一遍整理后的数组(收集)

再按照十位数字入队列,再取出...直到最高位

例:

【数据结构】九、排序_第6张图片【数据结构】九、排序_第7张图片【数据结构】九、排序_第8张图片【数据结构】九、排序_第9张图片【数据结构】九、排序_第10张图片

为什么从最低位开始比较?

虽然最高位最能决定数字大小,但先排高位后排低位会把总体的趋势打乱;先排低位,虽然高位相同的数字是分散的,但如果单独取出来看,会发现高位相同的数字间正变得有序。最后排高位,确定总体趋势,得到有序数组。

基数排序
时间效率 O(d(n+radix))      假设有 n 个记录,每个记录关键字有 d 位每个关键字的取值有radix个,则每趟分配需要的时间为O(n),每趟收集需要的时间为O(radix),合计每趟总时间为O(n+radix)。全部排序需要重复进行d 趟“分配”与“收集”。

空间效率

O(n+radix)     基数排序需要增加n+2radix个附加链接指针。存储数据的静态链表需要额外n个指针,每个静态队列需要头尾两个指针。
稳定性 稳定

代码

#include
#include

typedef struct {  //队列类型
    int head;
    int tail;
    int* base;
}array;   

void radix_sort(int* list, int len)
{ 
    int i, j, k;   //代表三层循环变量

    int high = 0;       //找最大值来计算元素的最高位数high
    int max = list[0];
    for (i = 0; i < len; i++)
        if (list[i] > max)
            max = list[i];
    while (max)
    {
        max /= 10;
        high++;
    }
   
    array* ten_arr = (array*)malloc(10 * sizeof(array));  //10个列表
    if (!ten_arr)  return;

    for (i = 0; i < 10; i++)  //列表初始化
    {
        ten_arr[i].head = ten_arr[i].tail = 0;
        ten_arr[i].base = (int*)malloc(sizeof(int) * 10);  //每个列表能放10个元素
        if (!ten_arr[i].base)  return;
    }
        
    int b = 1;  //b用于计算,表明当前取的是哪一位
    for (i = 0; i < high; i++)  
    {
        for (j = 0; j < len; j++)  //遍历数组元素
        {
            int number = (list[j] / b) % 10;    //取指定位数字
            ten_arr[number].base[ten_arr[number].tail++] = list[j];  //把元素放进对应队列
        }           
        b *= 10;  //b倍增,下次循环取下一位

        k = 0;
        for (j = 0; j < 10; j++)  //把队列中的数据收集起来放回列表中
        {
            while (ten_arr[j].head != ten_arr[j].tail) {
                list[k++] = ten_arr[j].base[ten_arr[j].head++];
            }               
            ten_arr[j].head = ten_arr[j].tail = 0;    //队列清空,这玩意不要放到while里面:(

        }
       
    }
}

int main(void)
{
    int i, len, * list;
    printf("数的个数:");
    scanf_s("%d", &len);
    list = (int*)malloc(len * sizeof(int)); 
    if (!list)  return NULL;

    printf("输入数:\n");
    for (i = 0; i < len; i++) 
        scanf_s("%d", &list[i]);

    radix_sort(list, len);

    for (i = 0; i < len; i++)
        printf("%d ", list[i]);

}

七、总结 

稳定的排序算法:鸡毛插龟壳(基数、冒泡、插入、归并

元素的移动次数与关键字的初始排列次序无关的是:基数排序

元素的比较次数与初始序列无关是:选择排序、折半插入排序

算法的时间复杂度与初始序列无关的是:选择排序、堆排序、归并排序、基数排序

算法的排序趟数与初始序列无关的是:插入排序、选择排序、基数排序

你可能感兴趣的:(数据结构,数据结构,算法,经验分享,排序算法,c++,c语言,笔记)