c++排序算法

0.算法分类

十种常见排序算法可以分为两大类:

  • 比较类排序:通过比较来决定元素间的相对次序,时间复杂度为 O(nlogn)~O(n²)。
  • 非比较类排序:不通过比较来决定元素间的相对次序,其时间复杂度可以突破 O(nlogn),以线性时间运行。

img

图片

名次解释

  • 时间/空间复杂度:描述一个算法执行时间/占用空间与数据规模的增长关系。

  • n:待排序列的个数。

  • k:“桶”的个数(上面的三种非比较类排序都是基于“桶”的思想实现的)。

  • In-place:原地算法,指的是占用常用内存,不占用额外内存。空间复杂度为 O(1) 的都可以认为是原地算法。

  • Out-place:非原地算法,占用额外内存。

  • 稳定性:假设待排序列中两元素相等,排序前后这两个相等元素的相对位置不变,则认为是稳定的。

1.冒泡排序(Bubble Sort)

冒泡排序(Bubble Sort),顾名思义,就是指越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1 算法描述

  • 从左到右,依次比较相邻的元素大小,更大的元素交换到右边;
  • 从第一组相邻元素比较到最后一组相邻元素,这一步结束最后一个元素必然是参与比较的元素中最大的元素;
  • 重复从左到后比较,而前一轮中得到的最后一个元素不参与比较,得出新一轮的最大元素;
  • 按照上述规则,每一轮结束会减少一个元素参与比较,直到没有任何一组元素需要比较。

1.2 动图演示

img

1.3 代码实现

void bubbleSort(int arr[],int n) {
	bool bExchange = false; // 交换标志
	for(int i=0;i<n-1;i++){ // 最多做n-1趟排序
		bExchange = false;
		for(int j=0;j<n-1-i;j++){
			if(arr[j]>arr[j+1]){
				swap(arr[j+1],arr[j]);
				bExchange = true; // 发生了交换,故将交换标志置为真
			}
		}
		if (!bExchange) // 考虑有一趟排序未发生交换的理想情况,可以提前终止算法
			return;
	}
}

1.4 算法分析

冒泡排序属于交换排序,是稳定排序,平均时间复杂度为 O(n²),空间复杂度为 O(1)。

但是我们常看到冒泡排序的最优时间复杂度是 O(n),那要如何优化呢?

上面就是优化后的代码,用了一个 bExchange 参数记录新一轮的排序中元素是否做过交换,如果没有,说明前面参与比较过的元素已经是正序,那就没必要再从头比较了,就可以优化到 O(n) 。

2. 选择排序(Selection Sort)

选择排序(Selection Sort)是一种简单直观的排序算法。它的基本思想就是,每一趟 n-i+1(i=1,2.....n-1)  个记录中选取值最小的索引作为有序序列的第 i 个索引。

2.1 算法描述

  • 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
  • 在剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾;
  • 重复步骤 2,直到所有元素排序完毕。

2.2 动图演示

img

2.3 代码实现

void selectSort(int arr[], int n){
	int minIndex = 0;
	int temp;
	for(int i=0;i<n-1;i++){ // 排序n-1次
		minIndex = i; // minIndex设置为每轮未排序序列的第一个位置
		for (int j=i+1;j<n;j++){
			// 每轮中最小的值索引,赋值给minIndex
			if (arr[j]<arr[minIndex]){
				minIndex = j;
			}
		}
		// 将每轮中最小的值与每轮中第一个位置(i)的值进行交换
		if (minIndex =i){ // 如果这轮中最小的值刚好在第一个位置,就不用交换了
			temp = arr[minIndex];
			arr[minIndex] = arr[i];
			arr[i] = temp;
		}
	}
}

2.4 算法分析

选择排序是不稳定排序,时间复杂度固定为 O(n²),因此它不适用于数据规模较大的序列。不过它也有优点,就是不占用额外的内存空间。

3. 插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1 算法描述

  • 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
  • 从头到尾依次扫描未排序序列,将扫描到的每个元素与有序序列的每个元素进行比较,小于哪个有序序列的元素就进行交换,相当于插入到该元素索引位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

3.2 动图演示

img

3.3 代码实现

void insertSort(int arr[], int n) {
	int i,j,temp;
	for(i=1;i<n;i++){
		temp = arr[i];
		for(j=i;j>0 && arr[j-1]>temp;j--)
			arr[j] = arr[j - 1]; // 把已排序元素逐步向后挪位
		arr[j] = temp; // 插入
	}
}

3.4 算法分析

插入排序在实现上,通常采用 in-place 排序(即只需用到 O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

 4. 希尔排序(Shell Sort)

1959年Shell发明,第一个突破 O(n2) 的排序算法,是插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

4.1 算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.2 动图演示

img

4.3 代码实现

void shellSort(int arr[],int size){
	int i,j,tmp,increment;
	for (increment=size/2;increment>0;increment/=2){
		for(i=increment;i<size;i++){
			tmp = arr[i];
			for(j=i-increment;j>=0&&tmp<arr[j];j-=increment){
				arr[j + increment] = arr[j];
			}
			arr[j+increment] = tmp;
		}
	}
}

4.4 算法分析

快速排序是不稳定排序,所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是 O(n²),它的平均时间复杂度为 O(n log n)。

 5. 归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2- 路归并。

5.1 算法描述

  • 把长度为 n 的输入序列分成两个长度为 n/2 的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

5.2 动图演示

img

5.3 代码实现

/* 将 arr[L..M] 和 arr[M+1..R] 归并 */
void merge(int arr[], int L, int M, int R) {
    int LEFT_SIZE = M - L + 1;
    int RIGHT_SIZE = R - M;
    int left[LEFT_SIZE];
    int right[RIGHT_SIZE];
    int i, j, k;
    // 以 M 为分割线,把原数组分成左右子数组
    for (i = L; i <= M; i++) left[i - L] = arr[i];
    for (i = M + 1; i <= R; i++) right[i - M - 1] = arr[i];
    // 再合并成一个有序数组(从两个序列中选出最小值依次插入)
    i = 0; j = 0; k = L;
    while (i < LEFT_SIZE && j < RIGHT_SIZE) arr[k++] = left[i] < right[j] ? left[i++] : right[j++];
    while (i < LEFT_SIZE) arr[k++] = left[i++];
    while (j < RIGHT_SIZE) arr[k++] = right[j++];
}

void merge_sort(int arr[], int L, int R) {
    if (L == R) return;
    // 将 arr[L..R] 平分为 arr[L..M] 和 arr[M+1..R]
    int M = (L + R) / 2;
    // 分别递归地将子序列排序为有序数列
    merge_sort(arr, L, M);
    merge_sort(arr, M + 1, R);
    // 将两个排序后的子序列再归并到 arr
    merge(arr, L, M, R);
}

5.4 算法分析

快速排序是不稳定排序,所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是 O(n²),它的平均时间复杂度为 O(n log n)。

 6. 快速排序(Quick Sort)

快速排序(Quick Sort),是冒泡排序的改进版,之所以“快速”,是因为使用了分治法。它也属于交换排序,通过元素之间的位置交换来达到排序的目的。

基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1 算法描述

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.2 动图演示

img

6.3 代码实现

void quickSort(int arr[], int begin, int end) {
	int i, j, t, pivot;
	if (begin > end) // 递归,直到start = end为止
		return;
	pivot = arr[begin]; // 基准数
	i = begin;
	j = end;
	while (i != j) {
		// 从右向左找比基准数小的数 (要先从右边开始找)
		while (arr[j] >= pivot && i < j)
			j--;
		// 从左向右找比基准数大的数
		while (arr[i] <= pivot && i < j)
			i++;
		if (i < j) {
			// 交换两个数在数组中的位置
			t = arr[i];
			arr[i] = arr[j];
			arr[j] = t;
		}
	}
	// 最终将基准数归位
	arr[begin] = arr[i];
	arr[i] = pivot;
	quickSort(arr, begin, i - 1); // 继续处理左边的,这里是一个递归的过程
	quickSort(arr, i + 1, end); // 继续处理右边的 ,这里是一个递归的过程
}

6.4 算法分析

快速排序是不稳定排序,所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是 O(n²),它的平均时间复杂度为 O(n log n)。

7.  计数排序(Counting Sort)

计数排序(Counting Sort)不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1 算法描述

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项;
  • 对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第 C(i) 项,每放一个元素就将 C(i) 减去 1。

8.2 动图演示

img

8.3 代码实现

void counting_sort(int arr[], int n) {
    if (arr == NULL) return;
    // 定义辅助空间并初始化
    int max = arr[0], min = arr[0];
    int i;
    for (i = 1; i < n; i++) {
        if (max < arr[i]) max = arr[i];
        if (min > arr[i]) min = arr[i];
    }
    int r = max - min + 1;
    int C[r];
    memset(C, 0, sizeof(C));
    // 定义目标数组
    int R[n];
    // 统计每个元素出现的次数
    for (i = 0; i < n; i++) C[arr[i] - min]++;
    // 对辅助空间内数据进行计算
    for (i = 1; i < r; i++) C[i] += C[i - 1];
    // 反向填充目标数组
    for (i = n - 1; i >= 0; i--) R[--C[arr[i] - min]] = arr[i];
    // 目标数组里的结果重新赋值给 arr
    for (i = 0; i < n; i++) arr[i] = R[i];
}

8.4 算法分析

计数排序属于非交换排序,是稳定排序,适合数据范围不显著大于数据数量的序列。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值