数据结构——排序(1)

目录

[1. 何为排序](#1. 何为排序)

[1.1 排序的概念](#1.1 排序的概念)

[1.2 排序的运用](#1.2 排序的运用)

[​编辑 1.3 常见的排序算法](#编辑 1.3 常见的排序算法)

2.常见排序算法的实现

[2.1 插入排序](#2.1 插入排序)

[2.1.1 直接插入排序](#2.1.1 直接插入排序)

[2.2.2 希尔排序](#2.2.2 希尔排序)

[2.2 选择排序](#2.2 选择排序)

[2.2.1 直接选择排序](#2.2.1 直接选择排序)

[2.2.2 堆排序](#2.2.2 堆排序)

[2.3 交换排序](#2.3 交换排序)

[2.3.1 冒泡排序](#2.3.1 冒泡排序)

[2.3.2 快速排序](#2.3.2 快速排序)


1. 何为排序

1.1 排序的概念

  • 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
  • 内部排序:数据元素全部放在内存中的排序。
  • 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 排序的运用

1.3 常见的排序算法


2.常见排序算法的实现

PS:本篇文章将会讲解插入排序,选择排序以及交换排序,其中交换排序中的快速排序的优化将会在下一篇文章中讲解,本篇文章只会讲解快速排序的Hoare方法,更详细的优化以及其他的一些排序将会在之后的文章中介绍。


2.1 插入排序

2.1.1 直接插入排序

直接插入排序就是把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

以下是一个动态的展示图:

那么具体应该如何实现呢?

cpp 复制代码
void InsertSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n - 1; ++i)
	{
        //end 就相当于已经排好的元素的个数
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
            //下一个位置的元素比end所在位置的元素要小,就把end的元素给到下一个位置上
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

测试:

cpp 复制代码
void InsertSortTest()
{
	int a[] = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
	int sz = sizeof(a) / sizeof(int);
	printf("排序前:");
	PrintArray(a, sz);
	printf("排序后:");
	InsertSort(a, sz);
	PrintArray(a, sz);
}


直接插入排序的特性总结:

  • 元素集合越接近有序,直接插入排序算法的时间效率越高
  • 时间复杂度:O(N^2)

2.2.2 希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个gap组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

动态展示如下:

实现方法:

方法一:

cpp 复制代码
void ShellSort(int* a, int n)
{
	int gap = n;
    //当gap > 1 时,将所有元素分成gap组,当==1的时候,就是直接插入排序
	while (gap > 1)
	{
        //gap没有专门的给法,通常采用 /3+1的方式
        //但是无论采用什么方法都要保证最后一次进循环的时候gap == 1
		gap = gap / 3 + 1; 
		int i = 0;
        //这里采用的是一组一组比较的形式
		for (i = 0; i < gap; ++i)
		{
			for (int j = i; j < n - gap; j += gap)
			{
				int end = j;
				int tmp = a[end + gap];
				while (end >= 0)
				{
					if (tmp < a[end])
					{
						a[end + gap] = a[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				a[end + gap] = tmp;
			}
		}
	}
	
}

方法二:

cpp 复制代码
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		int i = 0;
        //这里采用一个一个进行比较,注意不同
		for (i = 0; i < n - gap; ++i)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		
		}
	}
	
}

测试:

cpp 复制代码
void ShellSortTest()
{
	int a[] = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
	int sz = sizeof(a) / sizeof(int);
	printf("排序前:");
	PrintArray(a, sz);
	printf("排序后:");
	ShellSort(a, sz);
	PrintArray(a, sz);
}


希尔排序的特性总结:

  • 希尔排序是对直接插入排序的优化。
  • 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  • 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定

一般来说,希尔排序的时间复杂度是 O(N^1.3)。


2.2 选择排序

2.2.1 直接选择排序

  • 基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
  • 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素;
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换;
  • 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。

    实现方法:
cpp 复制代码
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int maxi = begin;
		int mini = begin;
        //i 从begin开始,到end结束,遍历一遍还未排序好的元素
        //并选出最大,最小元素所在的下标
		for (int i = begin; i <= end; ++i)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}
			if (a[maxi] < a[i])
			{
				maxi = i;
			}
		}
        // 与begin 和 end 所在的位置进行交换
		swap(&a[begin], &a[mini]);
        // 这里特别注意 当begin所在的位置就是最大值的时候,
        // 会在上一步先与a[mini]进行交换
        // 交换之后begin所在的元素就不是最大值了,而mini所在的位置变成了最大值,
        // 因此要加一层判断,将mini 赋值给 maxi 即可。
		if (begin == maxi)
		{
			maxi = mini;
		}
		swap(&a[end], &a[maxi]);
		++begin;
		--end;
	}
}

测试:

cpp 复制代码
void SelectSortTest()
{
	int a[] = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
	int sz = sizeof(a) / sizeof(int);
	printf("排序前:");
	PrintArray(a, sz);
	printf("排序后:");
	SelectSort(a, sz);
	PrintArray(a, sz);
}


直接选择排序的特性总结:

  • 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用;
  • 时间复杂度:O(N^2)。

2.2.2 堆排序

  • 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆****。
  • 这里运用到了堆的相关知识,学过二叉树的都知道,堆排序的思想就是先把所有的数据运用向下调整的方法建好堆,之后堆头元素和堆尾元素进行交换,在进行向下调整建堆。
  • 需要特别注意的是每次建堆都要采用向下调整建堆 而不是 向上调整,因为向下调整建堆的时间复杂度更低,效率更好,向下调整建堆 的时间复杂度是 O(N),而向上调整建堆 的时间复杂度是 O(N*logN)。
  • 动图如下:

  • 实现方法:
cpp 复制代码
void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void AdjustDown(int* a, int parent,int n)
{
    //先假设左孩子是两个孩子中较大的那个
	int child = (parent * 2) + 1;
	
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}
		if (a[parent] < a[child])
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = (parent * 2) + 1;
		}
		else
		{
			break;
		}
		
	}
}

//升序建大堆
void HeapSort(int* a, int n)
{
	int i = 0;
    // 每次向下调整建堆
	for (i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, i, n);
	}
	int j = 0;
    // 建堆完成后和堆尾元素交换,在进行向下调整建堆
    // 因为建大堆每次堆头元素都最大
	for (j = n - 1; j >= 0; --j)
	{
		swap(&a[0], &a[j]);
		AdjustDown(a, 0, j);
	}
}

测试:

cpp 复制代码
void HeapSortTest()
{
	int a[] = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
	int sz = sizeof(a) / sizeof(int);
	printf("排序前:");
	PrintArray(a, sz);
	printf("排序后:");
	HeapSort(a, sz);
	PrintArray(a, sz);

}


堆排序性能分析:

  • 堆排序使用堆来选数,效率就高了很多。
  • 时间复杂度:O(N*logN)

2.3 交换排序

2.3.1 冒泡排序

冒泡排序就不用太多赘述了,作为算法界的 "Hello World",直接看代码吧:

动图:

cpp 复制代码
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
        // 进入循环后,如果一次都没有交换,那么元素就有序了
        // 直接break就好。
		int exchange = 0;
		for (int j = 0; j < n - i - 1; ++j)
		{
			if (a[j] > a[j + 1])
			{
				swap(&a[j], &a[j + 1]);
				exchange = 1;
			}
		}
		if (exchange == 0)
		{
			break;
		}
	}
}

测试:

cpp 复制代码
void BubbleSortTest()
{
	int a[] = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
	int sz = sizeof(a) / sizeof(int);
	printf("排序前:");
	PrintArray(a, sz);
	printf("排序后:");
	BubbleSort(a, sz);
	PrintArray(a, sz);
}


冒泡排序的特性总结:

  • 冒泡排序是一种非常容易理解的排序
  • 时间复杂度:O(N^2)

2.3.2 快速排序

  • 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法;
  • 其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列;
  • 左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

本篇文章只讲解Hoare的快速排序,至于更优化的结构,将会再之后的文章中讲解。

动图如下:


实现方法:

cpp 复制代码
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	int left = begin;
    int right = end;
	int keyi = begin;

	while (left < right)
	{
		// 右边找小
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}

		// 左边找大
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}

		swap(&a[left], &a[right]);
	}

	swap(&a[left], &a[keyi]);
	keyi = left;

	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

注意堆排序这里有很多的坑:

  • 例如当left找大没找到或者right找小没找到,而且这个时候已经相遇了,但是left或者right还是会继续走,就会导致即使left和right相遇了也不会停下来的问题,因此要加一层逻辑判断,即 && left < right。
  • 在找大或者找小的过程中,只能找大或者找小,即使相等也要跳过,不然会产生死循环。
  • 正因为有这么多的缺陷,所以才要进行优化。优化的内容就之后再进行分解了哈哈哈。

测试:

cpp 复制代码
void QuickSortTest()
{
	int a[] = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
	int sz = sizeof(a) / sizeof(int);
	printf("排序前:");
	PrintArray(a, sz);
	printf("排序后:");
	QuickSort(a,0,sz-1);
	PrintArray(a, sz);
}


快速排序的特性总结:

  • 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  • 时间复杂度:O(N*logN)
相关推荐
时间幻灭‘28 分钟前
数据结构(2):LinkedList和链表[2]
数据结构·链表
CPP_ZhouXuyang1 小时前
C语言——模拟实现strcpy
c语言·开发语言·数据结构·算法·程序员创富
QXH2000001 小时前
数据结构—双向链表
c语言·数据结构·算法·链表
旺小仔.2 小时前
【数据结构篇】~排序(1)之插入排序
c语言·数据结构·算法·链表·性能优化·排序算法
尘心cx2 小时前
数据结构-顺序表
数据结构
问道飞鱼3 小时前
每日一个数据结构-跳表
数据结构
Crossoads3 小时前
【数据结构】排序算法---希尔排序
c语言·开发语言·数据结构·算法·排序算法
jingling5553 小时前
后端开发刷题 | 最长上升子序列
java·开发语言·数据结构·后端·算法·动态规划
Crossoads4 小时前
【数据结构】十大经典排序算法总结与分析
c语言·开发语言·数据结构·算法·排序算法
liangbm34 小时前
MATLAB系列04:循环结构
开发语言·数据结构·matlab·for循环·循环结构·工程基础·程序流程