C++实现经典排序算法(一)——插入排序和冒泡排序

排序算法应该是程序员面试的时候必然会被问到的问题之一。总结起来就下面几个问题:

  1. 你知道哪些排序算法?
  2. 具体介绍一下xx排序算法的实现原理
  3. xx排序算法的时间复杂度和空间复杂度分别是多少?
  4. xx排序算法是稳定的嘛?

面对以上几个问题,首先你得知道什么是排序,什么是时间/空间复杂度以及什么叫做稳定排序。首先,排序问题很好理解,就是将无序的一组数据变成有序的(可以是升序也可以是降序)。举个例子,< 3, 2, 5, 4, 7, 9, 0, 1>这一组数据经过你的代码处理后变成了<0, 1, 2, 3, 4, 5, 7, 9>他就完成了排序过程。其次就是时间复杂度和空间复杂度的问题。时间复杂度就是用来描述整个实现代码运行的时间,空间复杂度就是运行时消耗掉的空间(寄存器,内存)。这里不做过多的介绍,具体可以参考我的另一篇博客。最后一个就是所谓的稳定性,排序算法的稳定性是指,如果待排序数据中出现两个相同数a0 == a1,其中a0在a1的前面,排序完成之后,a0必然会和a1紧靠,但是此时a0必须还要在a1的前面,这种情况我们则称这种排序是稳定的,否则称之为不稳定排序。

有了以上基础知识后,我们先来介绍第一种排序算法:插入排序。插入排序算法是非常好理解的,想必看这篇文章的人都玩过斗地主吧(不接受抬杠,默认都玩过,没玩过的现在去买副扑克,找俩人一起玩一下)。在我们摸牌的时候,我们会将牌按照一定的次序排列,从左到右,由大到小或从右到左由大到小。我们总是摸一张牌后,和手上的牌依次做比较,直到找到这张牌的合适位置,并把它放入手中。这就是传说中的插入排序法。了解了其基本原理之后,我们可以写出如下伪代码:

void insertionSort(arr[], len){  //参数为待排序数组和数组的长度
	for i from 1 to len:  //遍历从第一个数开始,而不是第0个
		temp = arr[i]  //先将待插入数保存
		for j from i to 0 and arr[j - 1] > temp:  //从第i个数的前一个开始遍历如果前一个数比当前数字大,则将前一个数的值赋给当前数(搬迁)依次类推。 
			arr[j] = arr[j - 1]
		arr[j] = temp  //当上一个循环跳出之后,自然第几个数据所在的位置就是我们需要插入的位置,直接覆盖就好了。
}

同故宫上面的伪代码,我们可以确定整个算法的大致结构,那么接下来还有一个问题就是,需要比较什么类型的数据?这里可以参考C++中的template模板。有了模板之后,我们可以比较任何一个我们想比较的数据类型,包括类(只要我们重构">"操作符即可)。

代码如下:

#include<iostream>

using namespace std;

template<typename T>
void insertionSort(T arr[], int len) {  //参数为待排序数组和数组的长度
	for (int i = 1; i < len; i++) {  //遍历每一个带插入的数
		T temp = *(arr + i);  //将带插入数用中间变量保存
		int j;
		for (j = i; j > 0 && *(arr + j - 1) > temp; j--)  //从第i个数开始遍历,若i - 1个数小于第i个数,则搬迁。
		//for (j = i; j > 0 && *(arr + j - 1) >= temp; j--)  //不稳定排序算法
			*(arr + j) = *(arr + j - 1);
		*(arr + j) = temp;  //最后剩余的就是要插入的地方
	}
}

int main() {
	char a[] = { '1','5','3','4','6','5','3', '7', '0', 'a', 'B' };
	insertionSort(a, sizeof(a) / sizeof(char));
	for (auto p : a) {
		cout << p << "\t";
	}
}

还有几个关键性的问题,这种排序算法的时间复杂度和空间复杂度是多少呢?
我们考虑两种情况:

  1. 最好的情况就是,数组本来就排好序了,那么就不需要再排序了,我们的代码只需要检测一遍即可完成。即上述代码中最外层循环执行,内层循环不执行,因此时间复杂度为O(n)
  2. 最坏的情况就是,数组是逆序,那么我们就需要将每个数依次搬迁。这样不但最外层的循环要全部执行,内层的数据也要全部执行。因此时间复杂度为O(n2).

很明显,此处的空间复杂度为O(1). 一般地,插入排序法会用在数据量较小的情形下,当数据量过大时,插入排序算法的效率就显得非常低了。
除此之外,还需要判断一下插入排序是否属于稳定排序。从上面的代码来看,插入排序属于稳定排序算法,理由如下:
假设有这么一个数组:<1, 5, 3, 4, 6, 5, 3>,此处有两处相同的数。采用插入排序法排序时我们是从后往前找,只有找到比当前数要小的数,才插入。因此当我们找到第2个5时,只会将其放在第一个5后面,而不是前面。因此,插入排序法属于稳定排序算法。当然,插入排序算法也可以变成不稳定排序算法,只要修改一处地方——将上述代码中的第二个for循环采用下一行代替,可以思考一下为什么。

接下来,再来看看冒泡排序吧,冒泡排序法也很容易理解。所谓的冒泡就是指,每次将大数沉入底下,小数浮到上方,类似于鱼吐泡泡。因此叫冒泡排序。
冒泡排序的基本思路就是,遍历所有数据,依次两两比较,若大则交换,否则继续比较,其伪代码如下:

void bubbleSort(arr[], int len){
	for i from 0 to len:
		forj from i + 1 to len:
			if arr[i] < arr[j]:
				swap(arr[i], arr[j])
}

因此通过代码实现如下:

#include<iostream>

using namespace std;

template<typename T>
void bubbleSort(T arr[], int len) {  //参数为待排序数组和数组的长度
	for (int i = 0; i < len; i++) {  //遍历每一个带插入的数
		for(int j = i + 1; j < len; j++)
			if (*(arr + i) > * (arr + j)) {
				T temp = *(arr + j);
				*(arr + j) = *(arr + i);
				*(arr + i) = temp;
			}
	}
}

int main() {
	char a[] = { '1','5','3','4','6','5','3', '7', '0', 'a', 'B' };
	bubbleSort(a, sizeof(a) / sizeof(char));
	for (auto p : a) {
		cout << p << "\t";
	}
}

同理,冒泡排序法的时间复杂度最好情况也是O(n),最差情况也是O(n2),空间复杂度为O(1)以及属于稳定排序算法。当然也可以变成不稳定的,只需要修改一处地方即可。

猜你喜欢

转载自blog.csdn.net/WJ_SHI/article/details/106617764
今日推荐