冒泡排序
冒泡排序是低效的排序算法,通过不断交换 相邻逆序对 来实现排序。在该算法中因为越大的元素会经由交换慢慢“浮”到数列的顶端,故名冒泡排序。
冒泡排序一般是学到的第一个排序算法,实现较为简单,且容易理解。
冒泡排序的平均时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),最坏情况下时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。如果遍历时检查是否进行了交换,那么最好情况下时间复杂度为
O
(
n
)
O(n)
O(n)。
1. 常规冒泡排序
从头开始遍历,依次比较相邻的两个元素,如果两个元素大小关系不符合顺序则交换两个元素。每次遍历,都可以在剩余的元素中选出一个最值放到合适的位置上。
任意情况 | |
---|---|
复杂度 | O ( n 2 ) O(n^2) O(n2) |
void bubbleSort(int a[], int length)
{
for (int i = length - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
上面算法的缺点在于不管数列是否已经有序,都要继续遍历,不管数列中的值如何,时间复杂度都为
O
(
n
2
)
\ O(n^2)
O(n2)。
这对于小范围内无序的数列来说会花费过多不必要的时间。例如,仅仅是交换有序数列中某两个相邻的数,上述算法依然是要遍历
n
−
1
n-1
n−1次,实际仅需要遍历一次即可。下面的算法就是对这种情况做出的改进。
2. 可辨别数列有序的冒泡排序
对上面那种冒泡排序做了一个改进,设立了标志位,记录遍历过程中是否有发生过交换(有相邻逆序对就会交换)。如果遍历一次后没有发生过交换,说明每两个相邻元素之间都满足 a i ⩽ a i + 1 a_i \leqslant a_{i+1} ai⩽ai+1 的关系,那么整个数列的元素之间的关系是 a 0 ⩽ a 1 ⩽ a 2 ⩽ ⋯ ⩽ a n − 2 ⩽ a n − 1 a_0 \leqslant a_1 \leqslant a_2 \leqslant \cdots \leqslant a_{n-2} \leqslant a_{n-1} a0⩽a1⩽a2⩽⋯⩽an−2⩽an−1,此时数列有序,排序已经完成,可以停止遍历。
最坏情况 | 最好情况 | |
---|---|---|
复杂度 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) |
void bubbleSort(int a[], int length)
{
for (int i = length - 1; i > 0; i--) {
//遍历前初始化为:(没有进行过交换)
bool exchange = false;
for (int j = 0; j < i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
//记录此次遍历进行了交换
exchange = true;
}
}
// 没有进行过交换说明已经排序完成,退出
if (!exchange)
break;
}
}
这个算法只对混乱程度很小的数列较为有效,如果数列的元素值是随机的,那么在中间就完成排序的情况非常少,这样排序消耗的时间实际和常规冒泡排序差不多。
3. 鸡尾酒排序——改进的冒泡排序
鸡尾酒排序又称 双向冒泡排序,主要是通过发生交换的位置来确定有序区间和无序区间的分界点,缩小遍历区间,从而减少遍历时间。
最坏情况 | 最好情况 | |
---|---|---|
复杂度 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) |
在可辨别数列有序的冒泡算法中,仅仅是记录每次遍历是否进行过交换来判断数列是否有序。其实还可以记录发生交换时的位置。
在冒泡排序中(假设要按照升序排序),遍历时如果相邻两个元素大小关系不符,就会交换它们两个的位置,如果没有进行交换,说明它们两个的大小关系是符合的。如果在遍历的后半部分没有发生交换,说明这后半部分数已经有序了(如果有
a
⩽
b
a \leqslant b
a⩽b 且
b
⩽
c
b \leqslant c
b⩽c,那么有
a
⩽
b
⩽
c
a \leqslant b \leqslant c
a⩽b⩽c)。并且冒泡排序遍历时会把最大值一直往后移动,所以每一次交换,当前位置上一定是遍历过的部分的最值。而后面这部分没有发生交换,按照之前的
a
⩽
b
⩽
c
a \leqslant b \leqslant c
a⩽b⩽c 关系,这部分的值都会比前面这部分大(或者都相等)。因此这部分的元素已经是在正确位置上了,下一次就不需要再遍历了,即使遍历,也不会有更大的值来使这部分元素进行交换。
因此在遍历时将发生交换的位置设置为遍历的边界,只需要遍历边界以内的部分即可。同时,还能通过双向遍历的方式缩小两边的边界。等到左右边界重合或左边界大于右边界时,排序就完成了。
这个就是鸡尾酒排序算法,在数列混乱程度较小的情况下比常规冒泡排序快得多。
最坏情况下 | 最好情况下 | |
---|---|---|
复杂度 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) |
//鸡尾酒排序
void cocktailSort(int a[], int length)
{
int left = 0, right = length - 1; //遍历的范围
int bound;
while (left < right) {
//从左边界遍历至右边界,以最后一次交换的位置作为边界
bound = left; //初始值:如果没有发生交换,则取起始位置
for (int i = left; i < right; i++) {
if (a[i] > a[i + 1]) {
int temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
bound = i;
}
}
//修改右边界
right = bound;
//从右边界遍历至左边界,以最后一次交换的位置作为边界
bound = right;//初始值:如果没有发生交换,则取起始位置
for (int i = right; i > left; i--) {
if (a[i - 1] > a[i]) {
int temp = a[i - 1];
a[i - 1] = a[i];
a[i] = temp;
bound = i;
}
}
//修改左边界
left = bound;
}
}