排序算法总结(一)——O(n)时间复杂度排序

冒泡排序

初级版冒泡排序

冒泡排序(Bubble Sort) 一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。冒泡的实现在细节上可以有很多种变化,我们将分别就3种不同的冒泡实现代码,来讲解冒泡排序的思想。这里,我们就先来看看比较容易理解的一段。

void bubble_sort0(int a[],int n) {
	for (int i = 0; i < n; ++i) {
		for (int j = i+1; j < n; ++j) {
			if (a[i] > a[j]) {
				swap(a[i], a[j]);
			}
		}
	}
}

这段代码严格意义上说,不算是标准的冒泡排序算法,因为它不满足“ 两两比较相邻记录”的冒泡排序思想,它更应该是最最简单的交换排序而已。它的思路就是让每一个关键字,都和它后面的每一个关键字比较,如果大则交换,这样第一位置的关键字在一次循环后- -定变成最小值。 如图下图所示(图中i以1开始,代码中以0开始):

观察后发现,在排序好1和2的位置后,对其余关键字的排序没有什么帮助(数字3反而还被换到了最后一位)。也就是说,这个算法的效率是非常低的。

冒泡排序

void bubble_sort1(int a[], int n) {
	for (int i = 0; i < n - 1; ++i) {
		for (int j = n - 2; j >= i; --j) {
			if (a[j] > a[j + 1]) {
				swap(a[j], a[j + 1]);
			}
		}
	}
}

第一轮比较,将最小的数依次交换至最前面,如图:

第二轮比较再将第二小的数交换到第二个位置,以此类推。图中较小的数字如同气泡般慢慢浮到.上面,因此就将此算法命名为冒泡算法。

这里是将小的数从后往前冒泡,还可以将大的数从前往后冒泡,代码如下:

void bubble_sort2(int a[], int n) {
	for (int i = 0; i < n-1; ++i) {
		for (int j = 0; j < n - i - 1; ++j) {
			if (a[j] > a[j + 1]) {
				swap(a[j], a[j + 1]);
			}
		}
	}
}

冒泡排序优化

上面的冒泡程序是否还可以优化呢?答案是肯定的。试想一下,如果我们待排序的序列是{2,1,3,4,5,6,7,8,9},也就是说,除了第一和第二的关键字需要交换外,别的都已经是正常的顺序。当=1时,交换了2和1,此时序列已经有序,但是算法仍然不依不饶地将i=2到9以及每个循环中的j循环都执行了一遍,尽管并没有交换数据,但是之后的大量比较还是大大地多余了,如图下图所示。

为了避免多余的循环,我们增加一个标记变量flag,代码如下:

void bubble_sort3(int a[], int n) {
	bool flag = 1;
	for (int i = 0; i < n - 1 && flag == 1; ++i) {    //如果flag不为1则结束循环
		flag = 0;    //初始flag为0
		for (int j = n - 2; j >= i; --j) {
			if (a[j] > a[j + 1]) {
				swap(a[j], a[j + 1]);
				flag = 1;    //如果有数据交换,则将flag置为1
			}
		}
	}
}

时间复杂度分析

分析一下它的时间复杂度。当最好的情况,也就是要排序的表本身就是有序的,那么我们比较次数,根据最后改进的代码,可以推断出就是n-1次的比较,没有数据交换,时间复杂度为0(n)。 当最坏的情况,即待排序表是逆序的情况,此时需要比较1+2+3+...+(n-1)=n(n-1)/2次(改进前的冒泡排序比较次数始终为n(n-1)/2次),并作灯数量级的记录移动,因此,总的时间复杂度为O(n2)。

简单选择排序

选择排序的基本思想是每一趟在n-i+1(i=1,2,...,n- 1)个记录中选取关键字最小的记录作为有序序列的第i个记录。我们这里先介绍的是简单选择排序法。代码如下:

void select_sort(int a[], int n) {
	int min;
	for (int i = 0; i < n; ++i) {
		min = i;
		for (int j = i + 1; j < n; ++j) {
			if (a[min] > a[j]) {
				min = j;
			}
		}
		if (i != min) {
			swap(a[i], a[min]);
		}
	}
}

即第一轮比较找出最小的数将其于第一个位置的数交换,第二轮比较从除第一个以外的数中的最小数与第二个位置的数交换,以此类推,即第i轮比较找出第i小的数,放入第i个位置。

时间复杂度分析

从简单选择排序的过程来看,它最大的特点就是交换移动数据次数相当少,这样也就节约了相应的时间。分析它的时间复杂度发现,无论最好最差的情况,其比较次数都是一样的多,第i趟排序需要进行n-i次关键字的比较,此时需要比较

而对于交换次数而言,当最好的时候,交换为0次,最差的时候,也就初始降序时,交换次数为n- 1次,基于最终的排序时间是比较与交换的次数总和,因此,总的时间复杂度依然为0(n2)。应该说,尽管与冒泡排序同为0(n2),但简单选择排序的性能上还是要略优于冒泡排序。

直接插入排序

直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。代码如下:

void insert_sort(int a[], int n) {
	int k, j;
	for (int i = 1; i < n; ++i) {
		k = a[i];
		for (j = i - 1; a[j] > k; --j) {
			if (a[j] > k) {
				a[j + 1] = a[j];
			}
		}
		a[j + 1] = k;
	}
}

步骤1: 从第一个元素开始,该元素可以认为已经被排序;

步骤2: 取出下一个元素,并单独记录在k中,在已经排序的元素序列中从后向前扫描;

步骤3: 如果该元素(已排序)大于新元素,将该元素移到下一位置;

步骤4: 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

步骤5: 将新元素插入到该位置后;

步骤6: 重复步骤2~5。

复杂度分析 

我们来分析一下这个算法,从空间上来看,它只需要一个记录的辅助空间,因此关键是看它的时间复杂度。当最好的情况,也就是要排序的表本身就是有序的,比如{2,3,4,5,6},那么我们比较次数,其实就是每个a[i]与a[i- 1]的比较,共比较了n-1次,并且没有移动记录,时间复杂度为O(n)。

当最坏的情况,即待排序表是逆序的情况,比如{6,5,4,3,2},此时需要比较1+2+3+...+(n-1)=n(n-1)/2次,而记录的移动次数也达到了(n+4)(n-1)/2次。如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次数约为n^2/4次。因此,我们得出直接插入排序法的时间复杂度为0(n2)。从这里也看出,同样的0(n2)时间复杂度,直接插入排序法比冒泡和简单选择排序的性能要好一些。

发布了33 篇原创文章 · 获赞 148 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40692109/article/details/103186612