0%

数据结构冒泡排序

说到算法,不得不提到两个重要的概念:时间复杂度和空间复杂度。时间复杂度跟数据的输入规模 n 密切相关,时间复杂度的计算就是:找到执行次数最多的语句,然后计算语句执行次数的数量级,用大O表示结果。可以把这个输入规模看做是一个变量来理解,如果时间复杂度为O(1),那个这个 n 就不是一个变量而是一个常量,不管其数值是多少,哪怕是十万级,它的时间复杂度也是O(1),相反如果这个 n 是一个变量,可能它的最大值就是不过十,那么同样情况下(线性)它的时间复杂度就是O(n),二次级就是O(n^2),以此类推。讨论的时间复杂度是最坏情况下的时间复杂度,这就保证了算法的运行时间不会比任何更长。

空间复杂度是对一个算法在运行过程中临时占用存储空间的量度,当一个算法的空间复杂度为一个常量,即占用的临时存储空间不随被处理数据量n的大小而改变时,可表示为O(1)。

另外再说下排序算法的稳定性,通俗的讲就是排序前两个相等的数在序列中的前后位置顺序和排序后它们两个的前后位置顺序相同。如果A[i] = A[j],A[i] 原来在位置前,排序后A[i] 还是在A[j] 位置前,则是稳定的。这里的前后位置顺序是相对位置(不一定是相邻),排序前A[i] 在A[j] 前面,排序后A[i] 还是在A[j] 前面就是稳定的。
开始步入正题,以前学习过各大排序算法,现在又重新搬出来总结一下,并以博客的形式贴出来(全部按小到大排序),先总结最简单的冒泡程序。

冒泡排序名字的由来是因为越小的元素经过比较交换后会慢慢“浮”到数列的顶端,换句话说就是最大的数据“沉”到数列的最后,依次小一点的数据“沉”到数列的倒数位置,所以冒泡排序算法应时刻体现这个特点。由于排序算法众多,我们写出来的某种排序算法要的确是这种排序算法,不要写出来的所谓的希尔排序其实是个冒泡排序,那就四不像了。
原版:

设数组长度为N,

1) 比较相邻的前后两个数据,如果前面数据大于后面的数据,就将两者互换;

2) 这样对数组的第0个数据到第N-1个数据进行一次遍历后,最大的一个数据就“沉”到了数组第N-1个位置;

3) 接下来N=N-1,如果N不为0就重复前面两部,否则排序完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}

void BubbleSort(int Array[], int length)
{
for (int i = 0; i < length - 1; ++i)
for (int j = 1; j < length - i; ++j) //单次之后最大数据在最后面
{
if (Array[j - 1] > Array[j])
swap(Array[j - 1], Array[j]);
}
}

需要注意的是上面两个第一个 for 循环后面是 length - 1,因为最后两个数据只需比较一次就可以。上面这两个标准冒泡程序的时间复杂度都是O(N^2),输入规模为 n,两个for循环内部语句执行的次数的数量级为n^2。空间复杂度为O(1),因为运行过程中占用的临时存储空间不随数据输入规模n的大小而变化。冒泡排序算法是稳定的,相同元素的前后顺序并没有改变。
升级版一:

上面的冒泡程序还有升级的空间,如果输入数组本来就是已经排序好的或是大部分数据是已经排序好的,只需互换几次数据就可完成排序,那么就没必要再从头比较到尾了,我们可以设定一个标志位,如果单次遍历比较之后没有要交换数据的,说明已经排序好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void BubbleSort(int Array[], int length)
{
bool flag = true;
for (int i = 0; i < length - 1; ++i)
{
if (flag)
{
flag = false;
for (int j = 1; j < length - i; ++j)
{
if (Array[j - 1] > Array[j])
{
swap(Array[j - 1], Array[j]);
if (!flag)
flag = true;
}
}
}
}
}

上面的程序可以进一步简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void BubbleSort(int Array[], int length)
{
bool flag = true;
int n = length;

while (flag)
{
flag = false;
for (int i = 1; i < n; ++i)
{
if (Array[i - 1] > Array[i])
{
swap(Array[i - 1], Array[i]);
flag = true;
}
}
n--;
}

}

升级版二:
升级版一是考虑输入数组数据的特殊性,即只需比较互换几次数据就可完成排序的情况。再进一步,如果输入数组的数据恰好后半部分是排序好的,这样的话,就没必要重新遍历比较后半部分了,我们可以记录已排序好的位置,这样下一轮排序时只需遍历到该位置即可。后面这几个版本的程序都是将最大数据依次 “沉” 到最后,所谓的记录位置就是根据排序情况尽可能大的缩小遍历范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void BubbleSort(int Array[], int length)
{
int pos = length;
int n;

while (pos > 1)
{
n = pos; //设定遍历范围
pos = 0;
for (int i = 1; i < n; ++i)
{
if (Array[i - 1] > Array[i]) //较大的数据依次排序好在最后
{
swap(Array[i - 1], Array[i]);
pos = i; //记录已排序好的位置
}
}
}

}

虽然经过层层优化,但是冒泡程序的时间复杂度还是不优,后面优化的成效极大的取决于输入数组数据的特殊性。实际应用中,基本上不会涉及到冒泡排序,后面将会继续总结其他排序算法。

附冒泡排序链表版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};

void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}

bool BubbleSort(ListNode *phead)
{
if (phead == NULL)
return false;

for (ListNode *pre = phead; pre->next; pre = pre->next)
{
for (ListNode *pnext = pre->next; pnext; pnext = pnext->next)
{
if (pre->val > pnext->val)
swap(pre->val, pnext->val);
}
}
return true;

}
测试程序:
bool AddTail(ListNode **phead, int val)
{
if (phead == NULL)
return false;
if (*phead == NULL)
{
*phead = new ListNode(val);
return true;
}
ListNode *ptmp = *phead;
while (ptmp->next)
ptmp = ptmp->next;
ptmp->next = new ListNode(val);

return true;

}

int main()
{
ListNode *phead = NULL;

for (int i = 10; i >= 0; i--)
AddTail(&phead, i);

BubbleSort(phead);
return 0;