数据结构——排序

目录

一、排序算法的基本概念

1.1 排序算法的分类

1.1.1 内部排序

1.1.2 外部排序

二、插入排序

三、希尔排序

四、冒泡排序

五、快速排序

六、简单选择排序

七、堆排序

7.1 建立大根堆

7.2 基于大根堆进行排序

7.3 基于小根堆算法排序

7.4 堆的插入与删除

7.4.1 小根堆的插入

7.4.2 小根堆的删除

八、归并排序 

九、基数排序

十、外部排序

十一、败者树

总结


一、排序算法的基本概念

算法的稳定性。若待排序表中有两个元素Ri和Rj,其对应的关键字相同即keyi= keyj,且在排序前Ri在Rj的前面,若使用某一排序算法排序后,Ri仍然在Rj的前面,则称这个排序算法是稳定的,否则称排序算法是不稳定的。

1.1 排序算法的分类

1.1.1 内部排序

数据都在内存中

1.1.2 外部排序

数据太多,无法全部存入内存

二、插入排序

//插入排序算法
void insertsort(int a[], int n)
{
	int i, j, temp;
	for (i = 1; i < n; i++)
	{
		if (a[i] < a[i - 1])
		{
			temp = a[i];
			for (j = i - 1; j >= 0 && a[j] > temp; j--)//j跳出循环的时候为-1
			{
				a[j + 1] = a[j];
			}
			a[j + 1] = temp;
		}
	}
}

 这里可以采用(带哨兵)的优化算法,代码如下:

//直接插入排序 (带哨兵)
void InsertSort(int A[], int n)
{
	int i, j;
	for (i = 2; i <= n; i++)//依次将A[2]~A[n]插入到前面已排序序列
	{
		if (A[i] < A[i - 1]) //若A[i]关键码小于其前驱,将A[i]插入有序表
		{
			A[0] = A[i]; //复制为哨兵,A[0]不存放元素
			for (j = i - 1; A[0] < A[j]; --j) //从后往前查找待插入位置
				A[j + 1] = A[i];//向后挪位
			A[j + 1] = A[0]; //复制到插入位置
		}
	}
}

在已经有序的前提下,进行插入操作,还可以优化为折半插入排序,在使用折半插入排序时,当 low>high 时折半查找停止,应将 [ low , i-1] 内的元素全部右移,并将A[0]复制到 low 所指位置当 A[mid]==A[0] 时,为了保证算法的“稳定性”,应继续在 mid 所指位置右边寻找插入位置。

三、希尔排序

希尔排序: 先将待排序表分割成若干形如 L[ i , i+d , i+ 2d, ... , i+ kd ] 的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到 d =1为止。(步长 d 每次折半)

希尔排序代码如下:

//希尔排序
void shellsort(int a[], int n)
{
	int i, j, d;
	for (d = n / 2; d >= 1; d = d / 2)//步长变化
	{
		for (i = 1 + d; i < n; i++)
		{
			if (a[i] < a[i - d])//需要将a[i]插入有序增量子表
			{
				a[0] = a[i];//暂存在a[0]
				for (j = i - d; j > 0 && a[j] > a[0]; j = j - d)
				{
					a[j + d] = a[j];//记录后移,查找插入的位置
				}
				a[j + d] = a[0];//插入
			}
		}
	}
}

注意:希尔排序不稳定适用于顺序表,不适合链表。 

四、冒泡排序

算法如下:

void bubblesort(int a[], int n)
{
	for (int i = 0; i < n-1; i++)
	{
		int flag = 1;
		for (int j = 0; j <n-i-1; j++)
		{
			if (a[j] > a[j + 1])
			{
				int temp = 0;
				temp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = temp;
				flag = 0;
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
}

其中 flag 作为算法是否结束的标志,当某一趟比较不需要交换时,说明已经有序,可以直接跳出循环。

五、快速排序

算法思想: 在待排序表 L[1...n] 中任取一个元素 pivot 作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分 L[1...k-1] 和 L[k+1...n],使得L[1...k-1]中的所有元素小于pivot,L[k+1...n]中的所有元素大于等于 pivot,则 pivot 放在了其最终位置 L(k) 上,这个过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。

算法代码如下:

//确定枢轴元素在数组中的下标位置
//第一次执行时,用第一个元素将待排序序列划分成左右两个部分
int partition(int a[], int low, int high)
{
	int pivot = a[low];
	while (low < high)
	{
		while (low < high && a[high] < pivot)
			--high;
		while (low < high && a[low] > pivot)
			++low;
	}
	a[low] = pivot;
	return pivot;
}
//快速排序算法
void quicksort(int a[], int low, int high)
{
	if (low < high)//跳出递归的条件
	{
		int pivotpos = partition(a, low, high);//划分
		quicksort(a, low, pivotpos - 1);//划分左子表
		quicksort(a, pivotpos + 1, high);//划分右子表
	}
}

快速排序不稳定的,算法表现主要取决于递归深度“划分”越不均匀,递归深度越深若每次“划分”越均匀,则递归深度越低 。

六、简单选择排序

选择排序: 每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列

算法代码如下:

//简单选择排序
void selectsort(int a[], int n)
{
	for (int i = 0; i < n-1; i++)
	{
		int min = i;
		for (int j = i + 1; j < n; j++)
		{
			if (a[j] < a[min])
			{
				min = j;
			}
		}
		if (min != i)
		{
			int temp;
			temp = a[i];
			a[i] = a[min];
			a[min] = temp;
		}
	}
}

该算法不稳定适用性: 既可以用于顺序表,也可用链表 。

七、堆排序

判断 i 是否是叶子结点: i>n/2 ?

7.1 建立大根堆

思路: 1、把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
2、检查当前结点是否满足根>左、右。若不满足,将当前结点与更大的一个孩子互换
3、若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整 (小元素不断“下坠”)

建立大根堆代码如下:

//数组从下标1开始存数据
//建立大根堆
void buildmaxheap(int a[], int len)
{
	for (int i = len / 2; i > 0; i--)//从后往前调整所有非终端结点
		headadjust(a, i, len);
}
//将以k为根的子树调整为大根堆
void headadjust(int a[], int k, int len)
{
	a[0] = a[k];//a[0]暂存子树结点
	for (int i = 2*k; i < len; i++)//沿key较大的子节点向下筛选
	{ 
		if (a[i] < a[i + 1] && i < len)
			i++;
		if (a[i] <= a[0])
			break;
		else
		{
			a[k] = a[i];//将a[i]调整到双亲结点上
			k = i;//修改k值,以便继续向下筛查
		}
	}
	a[k] = a[0];//被筛结点值放入最终位置
}

7.2 基于大根堆进行排序

选择排序: 每一趟在待排序元素中选取关键字最大的元素加入有序子序列

堆排序: 每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换)

并将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

//堆排序完整逻辑
void heapsort(int a[], int len)
{
	buildmaxheap(a, len);
	for (int i = len; i > 1; i--)
	{
		swap(a[i], a[1]);
		headadjust(a, 1, i - 1);
	}
}

结论: 一个结点,每“下坠”一层,最多只需对比关键字2次

若树高为 h,某结点在第 i 层,则将这个结点向下调整最多只需要“下坠” h-i 层关键字对比次数不超过 2(h-i) 

n 个结点的完全二叉树树高  h=(log_{2}n)+1

该算法的时间复杂度为O(n),算法是不稳定的。

7.3 基于小根堆算法排序

 下面为以小根堆进行排序,算法如下。(小根堆排序得到递减序列)

//将以k为根的子树调整为小根堆
void headadjust(int a[], int k, int len)
{
	a[0] = a[k];//a[0]暂存子树结点
	for (int i = 2 * k; i < len; i=i*2)//沿key较大的子节点向下筛选
	{
		if (a[i] > a[i + 1] && i < len)
			i++;
		if (a[i] >= a[0])
			break;
		else
		{
			a[k] = a[i];//将a[i]调整到双亲结点上
			k = i;//修改k值,以便继续向下筛查
		}
	}
	a[k] = a[0];//被筛结点值放入最终位置
}
//建立小根堆
void buildminheap(int a[], int len)
{
	for (int i = len / 2; i > 0; i--)//从后往前调整所有非终端结点
		headadjust(a, i, len);
}

//堆排序完整逻辑
void heapsort(int a[], int len)
{
	buildminheap(a, len);
	for (int i = len; i >1; i--)
	{
		int temp = 0;
		temp = a[i];
		a[i] = a[1];
		a[1] = temp;
		headadjust(a, 1, i -1);
	}
}

7.4 堆的插入与删除

7.4.1 小根堆的插入

对于小根堆,新元素放到表尾,与父节点对比若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止。

7.4.2 小根堆的删除

被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止。

八、归并排序 

归并:把两个或多个已经有序的序列合并成一个。

归并排序: 在内部排序一般采用二路归并

核心操作: 把数组内的两个有序序列归并为一个

算法代码如下:

事先准备一个辅助数组B:int *B = (int*)malloc(n * sizeof(int));//辅助数组B

//a[low...mid]和a[mid+1...high]各自有序,将两个部分归并
void merge(int a[], int low, int mid, int high)
{
	int i, j, k;
	for (int k = low; k <= high; k++)
	{
		B[k] = a[k];//将A中所有元素复制到B中
	}
	for (int i = low, j = mid + 1, k = low; i <= mid, j <= high; k++)
	{
		if (B[i] > B[j])
		{
			a[k] = B[j++];//将较小值复制到A中
		}
		else
			a[k] = B[i++];
	}
	while (i <= mid)
		a[k++] = B[i++];
	while (j<=high)
		a[k++] = B[j++];
}
void mergesort(int a[], int low, int high)
{
	if (low < high)
	{
		int mid = (low + high) / 2;
		mergesort(a,low, mid);//对左半部分归并排序
		mergesort(a, mid+1, high); //对右半部分归并排序
		merge(a, low, mid, high);//归并
	}
}

九、基数排序

基数排序,时间复杂度 = O(d(n+r))

基数排序擅长解决的问题

1、数据元素的关键字可以方便地拆分为 d 组,且d较小

2、每组关键字的取值范围不大,即r较小

3、数据元素个数 n 较大

十、外部排序

重要结论: 采用多路归并可以减少归并趟数,从而减少磁盘I/O(读写)次数

对r 个初始归并段,做k路归并,则归并树可用 k 叉树表示。若树高为h,则归并趟数 = h-1 =log_{k}r

推导: k叉树第 h 层最多有k^{h-1}个结点则 r <= k^{h-1},(h-1)最小 = log_{k}r

十一、败者树

k路归并的败者树只需要定义一个长度为 k 的数组即可。(叶子结点为虚拟的)

败者树解决的问题: 使用多路平衡归并可减少归并趟数,但是用老土方法从 k 个归并段选出一个最小/最大元素需要对比关键字 k-1次,构造败者树可以使关键字对比次数减少到 log_{2}k

败者树可视为一棵完全二叉树(多了一个头头)。k个叶结点分别对应 k 个归并段中当前参加比较的元素,非叶子结点用来记忆左右子树中的“失败者”,而让胜者往上继续进行比较,一直到根结点。 


总结

可以登陆下面这个网站,查看各种算法的原理和演示过程。 

数据结构可视化 (usfca.edu)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值