[LeetBook]【学习日记】排序算法——时间复杂度O(n^2)

关于排序的稳定性

8种常见排序中(快排、希尔、堆排、归并、冒泡、选择、插入、基数排序)
快些(希尔)选堆不稳定,剩下的稳定

冒泡排序

普通冒泡排序要点

  1. 两个嵌套的循环
  2. 第一个循环 i 完成一轮就排序好一个元素,排序好的元素从后放置,如果总元素有 n 个,那么需要排序 n-1 个后就排序完成
  3. 第二个循环 j 用于遍历未排序完成的元素并进行比较,由于比较的为第 j 个和第 j+1 个元素,所以 j 范围只能从 0 到 n-1,也就是 j<n-1,这样 j+1 就刚好可以取到第 n-1 个,总共遍历了 n个元素
  4. j 的范围还可以进一步缩小,这是因为在第 i 轮循环时,有 i 个元素已经被排序好且放置在最后了,所以 j 可以不遍历这部分已经排序好的元素,故在 j<n-1的基础上去掉 i 个元素,也就是 j<n-i-1
  5. 使用标志 flag,当此轮外循环后没有交换,则数组绝对有序,不用再排序,即可退出排序
void bubbleSort(int arr[], int n)
{
	bool flag;
	for(int i=0; i<n-1; ++i){
		flag = false;
		for(int j=0; j<n-i-1; ++j){
			if(arr[j] > arr[j+1]){
				swapArrEle(&arr[j], &arr[j+1]);
				flag = true;
				
			}
			count++;
		}
		if(flag == false){
			break;
		}
	}
}

冒泡排序的进一步改进

  1. 和上文一样,使用 flag,在外循环没有发生交换时即认为数组有序
  2. 这次改进了内循环的范围,在冒泡排序中,外循环执行到第 k 轮,后面就有 k 个元素是必定有序的,但是有序的元素并不一定刚刚好是 k 个,可能是大于 k 个的,因此内循环可以不遍历所有已经有序的元素。
  3. 具体做法是,记录最后一个没有发生排序的元素的下标作为内循环范围,此下标等于最后一次发生交换的位置下标。原因:如果第 i 个下标发生了交换,而第 i+1 个下标往后没有发生交换,则 i+1 往后(包括本身)都是有序的,而第 i 个发生了交换,不一定是有序的,视作最后一个没有排序的下标
void bubbleSort2(int arr[], int n)
{
	bool swapped = true;
	int lastUnsortedEleIndex = n-1;//最后一个没有排序的元素下标
	int swappedIndex = -1;//上次发生交换的位置
	while(swapped){
		swapped = false;
		for(int i=0; i<lastUnsortedEleIndex; i++){
			if(arr[i]>arr[i+1]){
				swapArrEle(&arr[i], &arr[i+1]);
				swapped = true;//表示发生了交换
				swappedIndex = i;//更新交换的位置
				
			}
			count++;
		}
		//最后一个没有经过排序的元素的下标就是最后一次发生交换的位置
		lastUnsortedEleIndex = swappedIndex;
	}
	
}

性能分析

  • 上文代码使用了count记录进入内循环的次数,经笔者测试,改进的代码进入内循环的次数在一些情况下会比较少
  • 但是时间上,改进的代码不一定少,可能是因为改进代码中,在交换后需要更多的操作来记录交换的下标等信息
  • 测试代码:
int main() {
	int arr[] = {45, 23, 67, 12, 89, 34, 78, 54, 91, 27, 
		63, 16, 72, 39, 86, 56, 94, 31, 69, 42, 
		83, 50, 98, 37, 76, 60, 97, 28, 65, 19, 
		74, 47, 88, 33, 79, 58, 93, 36, 71, 51, 
		95, 30, 68, 22, 85, 53, 90, 26, 62, 15, 
		73, 40, 87, 55, 92, 32, 70, 49, 96, 29, 
		66, 18, 75, 44, 80, 57, 99, 38, 77, 61, 
		24, 64, 20, 25, 46, 81, 52, 17, 35, 59, 
		82, 48, 42, 67, 11, 86, 92, 39, 78, 54, 
		91, 26, 73, 19, 68, 46, 83, 42, 79, 35
		
	};
	
	int n = sizeof(arr)/sizeof(arr[0]);
	
	cout << "排序前:" << endl;
	printArr(arr, n);
	
	bubbleSort(arr, n);
	
	cout << "排序后:" << endl;
	printArr(arr, n);
	
	cout << count << endl << endl;
	
	int arr2[] = {45, 23, 67, 12, 89, 34, 78, 54, 91, 27, 
		63, 16, 72, 39, 86, 56, 94, 31, 69, 42, 
		83, 50, 98, 37, 76, 60, 97, 28, 65, 19, 
		74, 47, 88, 33, 79, 58, 93, 36, 71, 51, 
		95, 30, 68, 22, 85, 53, 90, 26, 62, 15, 
		73, 40, 87, 55, 92, 32, 70, 49, 96, 29, 
		66, 18, 75, 44, 80, 57, 99, 38, 77, 61, 
		24, 64, 20, 25, 46, 81, 52, 17, 35, 59, 
		82, 48, 42, 67, 11, 86, 92, 39, 78, 54, 
		91, 26, 73, 19, 68, 46, 83, 42, 79, 35
		
	};
	count = 0;
	
	int n2 = sizeof(arr2)/sizeof(arr2[0]);
	
	cout << "改进排序前:" << endl;
	printArr(arr2, n2);
	
	bubbleSort2(arr2, n2);
	
	cout << "改进排序后:" << endl;
	printArr(arr2, n2);
	
	cout << count << endl << endl;
	
	return 0;
}

在这里插入图片描述

选择排序

要点

  1. 同冒泡排序一致,排序 length - 1 元素则 length 个元素都排序好,所以外循环是 n-1 次,第 i 次外循环则有 i 个元素在数组前排序好
  2. 内循环则是找出最小元素的下标
void selectSort(int arr[], int n)	
{
	int minIndex;
	for(int i=0; i<n-1; ++i){
		minIndex = i;
		for(int j=i+1; j<n; ++j){
			if(arr[j]<arr[minIndex]){
				minIndex = j;
			}
			count++;
		}
		if(minIndex != i){
			swapArrEle(&arr[i], &arr[minIndex]);
		}
	}
}

改进:二元选择排序

  1. 一个内循环同时找出最大值下标和最小值下标
  2. 排序好的元素分别放在两端
  3. 当找到的最小值下标与最大值下标相同时,结束外循环。原因:内循环遍历未排序完成的元素时,只有一种情况会使这两种下标相同,也就是未排序部分元素相同,此时这些相同元素也放置在正确的位置上,排序完成
  4. 先放置最小值下标后,如果最大值的下标是i,而i刚才与最小下标被交换了,所以要更正最大值下标
void selectSort2(int arr[], int n)
{
	int minIndex;
	int maxIndex;
	for(int i=0; i<n/2; ++i){
		minIndex = i;
		maxIndex = n-1-i;
		for(int j=i+1; j<n-i; ++j){
			if(arr[j] < arr[minIndex]) minIndex = j;
			if(arr[j] > arr[maxIndex]) maxIndex = j;
			count++;
		}
		if(minIndex == maxIndex) break;
		
		swapArrEle(&arr[i], &arr[minIndex]);
		
		//如果最大值的下标是i,而i刚才与最小下标被交换了,所以要更正最大值下标
		if(maxIndex == i) maxIndex = minIndex;
		swapArrEle(&arr[n-1-i], &arr[maxIndex]);
	}
}

插入排序

交换法插入排序要点

  1. 两层循环
  2. 外循环从第二个数开始遍历
  3. 内循环将本次外循环遍历到的数依次与前面的数字比较,符合条件的就交换
  4. 由于是交换,所以无需其他内存空间,但是缺点是本次交换到的位置并非总是最合适的,会发生多次无效交换
//交换法插入排序
void insertSort(int arr[], int n)
{
	for(int i=1; i<n; ++i){//从第二个数开始与前面比较并插入
		int j=i;//j作为本轮遍历用的下标
		while(arr[j]<arr[j-1] && j>=1){
			swapArrEle(&arr[j], &arr[j-1]);
			--j;
		}
	}
}

移动法插入排序要点

  1. 为了不重复交换,核心思想改为与前面的元素比较,比其大的就往后挪,直到挪出一个合适的位置就插入
  2. 所以需要额外内存来记录本轮需要比较的数字
//移动法插入排序
void insertSort2(int arr[], int n)
{
	for(int i=1; i<n; ++i){
		int j=i;
		int currEle = arr[i];
		while(j>=1 && currEle<arr[j-1]){
			arr[j] = arr[j-1];
			--j;
		}
		arr[j] = currEle;
	}
}

冒泡、选择、插入排序比较

  • 在这三种排序中,选择排序交换次数是最少的
  • 在数组几乎有序的情况下,插入排序的时间复杂度最接近线性级别

两数交换不使用临时变量

如果考虑避免移除,应使用按位异或运算:

arr[j] = arr[j] ^ arr[j-1];
arr[j-1] = arr[j] ^ arr[j-1];
arr[j] = arr[j] ^ arr[j-1];
  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

__Witheart__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值