第8章 排序

一、插入排序

基本思想:每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列,直到全部记录插入完成

直接插入排序

时间复杂度:最好O(n):表中元素有序,最坏O(n2):表中元素逆序

空间复杂度:O(1)

稳定性:稳定,总是插入到相同元素的后面

适用性:顺序、链式(从前往后查找指定元素位置)

代码

void InsertSort(ElemType A[],int n) //下标:1~n,0为哨兵
{
	int i,j;
	for(i = 2;i <= n;i++)
	{
		if(A[i] < A[i-1]) //若待排元素大于前面已排元素,说明有序,无需挪动
		{
			A[0] = A[i]; //哨兵
			for(int j = i-1;A[j] > A[0];j--) //在已排序列中寻找位置
			{
				A[j+1] = A[j];//边比较边移动元素
			}
			A[j+1] = A[0];//插入操作,注意:下标需要加1
		}
	}
}

折半插入排序 小数据量性能好

时间复杂度:最好O(n):表中元素有序,最坏O(n2):表中元素逆序

  • 虽然折半插入排序可以O(logn)的复杂度找到插入位置,但挪动元素仍为O(n),因此时间复杂度仍为O(n2)
  • 数据量较小时性能好

空间复杂度:O(1)

稳定性:稳定,总是插入到相同元素的后面

适用性:仅顺序

代码

void InsertSort(ElemType A[],int n)
{
	int i,j,low,high,mid;
	for(int i = 2;i <= n;i++)
	{
		if(A[i] < A[i-1])
		{
			A[0] = A[i]; //哨兵
			low = 1,high = i - 1;//设置折半查找范围
			while(low <= high)
			{
				mid = (low + high) / 2;
				if(A[mid] > A[0]) high = mid - 1;//查找左半子表
				else low = mid + 1; //查找右半子表,注意:相等时查找右半子表是为了保证排序的稳定性
			}
			//循环后low位于high右边
			for(j = i - 1;j >= low;j--) //统一后移元素,腾出位置
			{
				A[j + 1] = A[j];
			}
			A[low] = A[0]; //插入操作
		}
	}
}

希尔排序 重点掌握思想且会手动模拟

时间复杂度:最坏O(n2)

空间复杂度:O(1)

稳定性:不稳定

反例:2 2 1 第一趟排序后1 2 2

适用性:仅顺序

代码

void ShellSort(ElemType A[],int n)
{
	int dk,i,j;
	for(dk = n/2;dk >= 1;dk = dk / 2) //增量变化(无统一规定)
	{
		for(i = dk + 1;i <= n;i++)
		{
			if(A[i] < A[i - dk])
			{
				A[0] = A[i]; //哨兵
				for(j = i - dk;j > 0 && A[0] < A[j];j = j - dk)
				{
					A[j + dk] = A[j]; //边查找边后移
				}
				A[j + dk] = A[0]; //插入,注意:下标为j + dk
			}
		}
	}
}

二、交换排序:每次排序能确定一个元素的最终位置

冒泡排序

时间复杂度:最好O(n),最坏O(n2)

最好情况:元素基本有序
最坏情况:元素逆序

空间复杂度:O(1)

稳定性:稳定

当i>j且A[i] = A[j]时,不会发生交换,因此是稳定的

适用性:顺序、链式

代码

void BubbleSort(LL A[],int n)
{
	int i,j;
	bool flag = false; //表示本趟冒泡是否发生交换
	for(i = 1;i <= n - 1;i++) //表示每趟交换的左端点,单个元素无需交换,故n-1
	{
		flag = false;
		for(j = n;j > i;j--) //从右往左依次交换
		{
			if(A[j] < A[j-1]) //逆序则两两交换
			{
				flag = true;
				swap(A[j],A[j-1]);
			}
		}
		if(flag == false) return;//若本趟遍历没有发生交换,说明已经有序
	}
}

快速排序 基于分治法 出选择,给出某趟排序的结果

特点:基于分治法;平均性能最优的内部排序算法;每次确定一个元素最终位置

时间复杂度:最好O(nlogn),最坏O(n2)

  • 快排是所有内部排序算法中平均性能最优的排序算法
  • 快排的时间性能主要取决于划分操作的好坏
  • 最好情况:每次均匀划分,logn个序列,每个序列遍历一次 O(nlogn)
  • 最坏情况:序列基本有序或逆序,每次划分出长度为n-1和1的序列 O(n2)

空间复杂度:最好O(logn),最坏O(n)

  • 快排是递归的,因此需要系统栈来保存每层调用的信息,其容量与递归调用的最大深度有关

稳定性:不稳定

反例:2 2 1 一趟排序后:1 2 2

适用性:仅顺序结构

代码

标准版

void QuickSort(ElemType A[],int low,int high)
{
	if(low < high)
	{
		int pivotpos = Partition(A,low,high);
		QuickSort(A,low,pivotpos - 1);
		QuickSort(A,pivotpos + 1,high);
	}
}

int Partition(ElemType A[],int low,int high)
{
	ElemType pivot = A[low];//将当前表中第一个元素设为枢纽,进行划分
	while(low < high)
	{
		while(low < high && A[high] >= pivot) high--;
		A[low] = A[high];//将比枢纽小的元素移动到左端
		while(low < high && A[low] <= pivot) low++;
		A[high] = A[low];//将比枢纽大的元素移动到右端
	}
	A[low] = pivot;//枢纽元素存放到最终位置
	return low;//返回枢纽的最终位置
}

简洁版

void Quick_Sort(LL a[],int low,int high)
{
    if(low < high)
    {
        int l = low,r = high,pivot = a[low];
		//划分操作
        while(l<r)
        {
            while(l < r && a[r] >= pivot) --r;
            a[l] = a[r];
            while(l < r && a[l] <= pivot) ++l;
            a[r] = a[l];
        }
        a[l] = pivot;
        Quick_Sort(a,low,l-1);
        Quick_Sort(a,l+1,high);
    }
   return;
}

三、选择排序

基本思想:每趟排序中在后面n-i+1个待排元素中选取关键字最小的元素,作为有序子序列的第i个元素,直至n-1趟做完。

简单选择排序

时间复杂度:始终O(n2)

空间复杂度:O(1)

稳定性:不稳定

反例:2 2 1 第一趟排序后1 2 2

适用性:顺序、链式(仅交换结点信息)

代码

void SelectSort(ElemType A[],int n) //A[0 ~ n-1]
{
	for(int i = 0;i < n - 1;i++) //共进行n-1趟排序
	{
		int Min = i;//记录最小值下标
		for(int j = i+1;j < n;j++) //在A[i ~ n-1]中选择最小值
		{
			if(A[j] < A[Min]) Min = j;//更新最小元素位置
		}
		if(Min != i) swap(A[Min],A[i]); //共移动3次元素
	}
}

堆排序:时间复杂度始终O(nlogn),空间复杂度只需O(1),优于快排的地方

时间复杂度:始终为O(nlogn)

建堆时间为O(n),之后有n-1次调整操作,每次调整时间复杂度为O(h)

空间复杂度:O(1)

稳定性:不稳定

反例:1 2 2 3,最终排序结果为3 2 2 1

适用性:顺序

本质是一棵完全二叉树

插入操作 O(log2 n)

1.先将元素插入到堆的末尾,即最后一个孩子之后

2.插入之后如果堆的性质遭到破坏,将新插入的节点顺着双亲往上调整到合适的位置即可

删除操作(堆的删除一定删除的是堆顶元素)

1.将堆顶元素与最后一个元素交换

2.将堆中有效元素个数减少一个

3.对堆顶元素向下调整

代码

标准版

建立大根堆

void HeadAdjust(ElemType A[],int k,int len) //将元素k为根的子树进行调整
{
	A[0] = A[k]; //A[0]暂存子树的根结点
	for(int i = 2*k;i <= len;i = i * 2) //沿k较大的子结点向下筛选
	{
		if(i+1 <= len && A[i+1] > A[i]) ++i;//选择较大的子结点
		if(A[0] >= A[i]) break;//若符合堆得要求,筛选结束
		else
		{
			A[k] = A[i];//否则将子结点上浮至双亲结点
			k = i;//修改根结点的下标,以便继续向下筛选
		}
	}
	A[k] = A[0];//被筛选结点放入最终位置
}

void BuildMaxHeap(ElemType A[],int len)
{
	for(int i = len / 2;i >= 1;i--) //i=[i/2] ~ 1反复调整根
	{
		HeadAdjust(A,i,len);
	}
}

void HeapSort(ElemType A[],int len)
{
	BuildMaxHeap(A,len);
	for(int i = len;i >= 1;i--)
	{
		print(A[1]);
		swap(A[1],A[i]);
		//输出并交换堆顶元素
		HeapAdjust(A,1,i - 1);//调整,把剩余i-1个元素整理成堆
	}
}

简洁版

建立小根堆并排序

void HeapAdjust(LL a[],int k,int len)
{
    a[0] = a[k];
    for(int i = 2*k;i<=len;i*=2)
    {
        if(i < len && a[i] > a[i+1]) ++i;
        if(a[0] < a[i]) break;
        else
        {
            a[k] = a[i];
            k = i;
        }
    }
    a[k] = a[0];
}

void HeapSort(LL a[],int len)
{
    for(int i = len/2;i>=1;i--)
    {
        HeapAdjust(a,i,len);
    }
    for(int i = len;i>=1;i--)
    {
        cout<<a[1];
        if(i>1) cout<<' ';
        swap(a[1],a[i]);
        HeapAdjust(a,1,i-1);
    }
}

四、归并排序 基于分治法

时间复杂度:始终O(nlogn)

空间复杂度:O(n)

稳定性:稳定

适用性:顺序、链式

排序过程

image

代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5+10;
int a[N],tmp[N],n;

void merge_sort(int a[],int l,int r)
{
	if(l>=r) return;//区间元素个数不大于1,不需要排序 
	int mid = l+r>>1;
	merge_sort(a,l,mid),merge_sort(a,mid+1,r);//折半拆分一个区间 
	int i = l,j = mid+1,k = 0;
	while(i<=mid && j<=r) //合并后的两个序列一定是有序的,双指针算法进行选择 
	{
		if(a[i] <= a[j]) tmp[k++] = a[i++];
		else tmp[k++] = a[j++];
	}
	while(i<=mid) tmp[k++] = a[i++];//两个序列长度可能不一致,因此把剩余的加入临时数组 
	while(j<=r) tmp[k++] = a[j++];
	for(int i=l,j=0;i<=r;i++,j++) //排序完毕,将临时数组赋值给原数组,区间[l,r]排序完成 
	{
		a[i] = tmp[j];
	}
}

int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
	{
		cin>>a[i];
	}
	merge_sort(a,0,n-1);
		for(int i=0;i<n;i++)
	{
		cout<<a[i]<<" ";
	}
	return 0;
}

五、基数排序 稳定 基于关键字各位的大小进行比较 多关键字排序思想

主要步骤

设长度为n的线性表中每个结点的关键字由d元组组成,基数为r。通常采用链式基数排序

  • 分配:开始时,把Q0~Qr-1各个队列置成空队列,然后依次遍历线性表中的每个结点,若结点的关键字为k,则把该关键字放入Qk队列中
  • 收集:把Q0~Qr-1各个队列中的结点依次首尾相接,得到新的结点序列,从而组成新的线性表
  • 共进行d趟分配和收集

时间效率

共进行d趟分配和收集,一趟分配需要O(n),一趟收集需要O(r),故时间复杂度为O(d(n+r)),与序列初始效率无关

空间效率

一趟排序需要r个队列,空间复杂度为O(r)

稳定性:必须是稳定的

六、各种内部排序算法的比较及应用

image

小结

  • 若n较小,可采用直接插入排序或简单选择排序。直接插入排序的记录移动次数较简单选择排序多,因此当记录本身信息量较大时,用简单选择排序较好
  • 若记录初始状态基本有序,则采用直接插入排序冒泡排序较好
  • 若n较大,则应采用O(nlogn)的算法:快速排序、堆排序、归并排序。其中快排被认为是基于比较的内部排序方法中最好的方法
  • 当待排序列随机分布时,快速排序的平均时间最短;堆排序所需的辅助空间少于快速排序,且不会出现快排的最坏情况,二者均是不稳定的排序方法
  • 若要求时间O(nlogn)且稳定的算法,则选用归并排序
  • 任何借助于“比较”的排序算法,至少需要O(nlogn)的时间
  • 若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好
  • 当记录本身信息量很大,为避免大量时间移动记录,可用链表作为存储结构

七、易错总结

  • 冒泡排序的比较次数是序列中逆序对个数之和
  • 快速排序每趟至少能确定一个枢轴元素的最终位置,也可能确定多个,取决于枢轴对数组的划分
  • 快速排序最快情形是观察枢轴元素(一般的各序列的首元素)对序列的划分是否均匀,最慢情形是基本有序或逆序
  • 快排每趟过程中移动记录次数即不在原位置上的元素的个数
  • 若题目未明确说明升序或降序排列,则都有可能,都要考虑
  • 可以用队列来记忆递归函数的两个端点的参数,只是排序过程中序列处理顺序不同,结果相同
  • 若只对两/三类元素进行排序(如所有奇数移动到偶数前/按照红白蓝的顺序排列条块等),一般采用快速排序的划分思想
  • 堆排序中,建堆的时间复杂度为O(n)
  • 在含有n个关键字的小根堆中,关键字最大的记录可能在[2/n + 1,n]中,因为最大值在底部叶结点中,而完全二叉树的叶结点编号范围为[2/n + 1,n]
  • 复杂度与初始状态无关的排序算法有:选择排序、归并排序、堆排序、基数排序
  • 比较次数与序列初始状态无关的排序算法有:选择排序归并排序基数排序
  • 移动次数与序列初始状态无关的排序算法有:归并排序、基数排序
  • 排序趟数与序列初始状态有关的排序算法有:冒泡排序、快速排序
  • 将两个长度分别为M和N的有序表合并成一个有序表(M < N),最少比较次数:M,最多比较次数:M + N - 1
  • 多关键字排序中,先排优先级较低的,再排优先级较高的,按照优先级升序排列
posted @ 2023-10-05 20:45  安河桥北i  阅读(1)  评论(0编辑  收藏  举报