1.常见排序算法分析
(1)选择排序
算法原理:
- 从数组 a[1--n] 中找到最小的元素,将其与第一个元素 a[1] 进行值的交换,这样数组中第一个位置就是最小的元素,即第一个位置变得有序。
- 接着从数组 a[2--n] 中找到最小的元素,将其与第二个元素 a[2] 进行值的交换,这样数组中前两个位置就是最小的两个元素,即前两个位置变得有序。
- 以此类推,每次从未排序的部分中选择最小的元素,将其与当前位置进行交换,使得数组的前面部分是有序的,并且有序的部分随着算法的不断迭代增大。
- 最终经过 n-1 次迭代之后,整个数组变得有序。
伪代码实现:
选择排序
1:Function Selection_Sort()
2: For i=1 to n-1 //n-1次迭代
3: Min_idx=i //记录a[i--n]最小值的下标,初始为i
4: For j=i+1 to n //在a[i+1--n]中找到最小值的下标
5: If a[j]<a[min_idx]
6: Min_idx=j
7: Swap(a[i],a[Min_idx]) //a[i--n]中的最小值与a[i]交换
时间复杂度分析:
- 外层循环执行了n-1次迭代。
- 内层循在每次外层循环中执行了n-i次迭代,并执行比较更新和交换常数操作。
- 总的时间复杂度表示:
- 可以看到T(n)的主要部分是n^2,所以选择排序的时间复杂度为O(n^2)。
- 无论是最优情况还是最坏情况,选择排序都需要执行两层嵌套的循环,外层循环控制迭代次数,内层循环用于在未排序部分中找到最小值,时间复杂度都为O(n^2)。
实际性能与预测性能对比:可以发现实际性能与预测性能是比较符合的,两条曲线几乎吻合。
优化性能测试:
- 考虑优化,单取最小值与同时取最大值和最小值,同时取最大值和最小值是每次迭代中同时找到最大值和最小值,并将他们交换到正确的位置上。
- 例子如下,设原序列为{9,8,2,6,3},蓝色代表当前最小值,红色代表当前最大值,灰色代表已经排序后的数,3趟即可完成。
- 同时取最大值和最小值的选择排序算法的伪代码如下
选择排序优化 1:Function Selection_Sort_withMaxMin(a[],begin,end) 2: n=end-begin+1 3: For i=1 to n 4: //设置min_idx和max_idx初始值为当前位 5: min_idx=i 6: max_idx=i 7: For j=i+1 to n //在当前未排序部分找到最小值和最大值的下标 8: If a[j]<a[min_idx] min_idx=j 9: If a[j]<a[min_idx] min_idx=j 10: Swap(a[i],a[min_idx]) 11: If max_idx==i //如果最大值的下标与当前位置相同,则更新 12: max_idx=min_idx //max_idx为最小值的下标 13: Swap(a[n],a[max_idx]) 14: //将未排序的部分的最后一个元素与最大值交换
- 对比图如下:
-
可以发现实际运行中,优化后的性能反而运行更慢了,这是因为虽然减少了比较次数,不过需要进行两次交换操作,增加了交换的开销,所测数据应该是交换操作次数比比较次数多太多,反而花费了额外的时间,使得运行时间更久。
(2)冒泡排序
算法原理:
- 遍历数组中的每对相邻元素 (a[j], a[j+1]),其中 i 和 j 分别表示数组的索引,范围为j∈[1, n-i], i∈[1, n-1]。
- 对于每一对相邻元素 (a[j], a[j+1]),比较它们的值。如果 a[j] 大于 a[j+1],则交换它们的位置,即将较大的元素向后移动。
- 通过重复执行步骤 1 和步骤 2,可以确保每一趟冒泡都会将当前未排序部分的最大元素“冒泡”到合适的位置。以此类推,使得数组的后面部分是有序的,并且有序的部分随着算法的不断迭代不断增大。
- 经过 n-1 趟冒泡之后,整个数组会变得有序。
伪代码实现:
冒泡排序 |
|
时间复杂度分析:
- 外层循环执行了n-1趟冒泡。
- 内层循环在每次外层循环中执行了n-i次迭代,执行比较和交换常数时间操作。
- 总的时间复杂度表示:
- 可以看到T(n)的主要部分是n^2,所以冒泡排序的时间复杂度为O(n^2)。
- 在最优情况下,冒泡排序仍然需要执行 n−1 趟冒泡操作,但在每一趟冒泡中都不需要进行任何元素的交换,因为数组已经是有序的。尽管如此,每趟冒泡仍需完整遍历一次未排序部分,因此时间复杂度仍然是 O(n^2)。
实际性能与预测性能对比:可以发现实际性能与预测性能是比较符合的,两条曲线比较贴近,可以验证得算法的正确性。
优化性能测试:
- 有序性检查优化,如果在一趟遍历中没有发生元素之间的交换,则说明序列已经有序,可以直接退出排序过程。伪代码如下:
冒泡排序优化--有序性检查优化 |
|
- 双向冒泡排序,可以同时从序列的两端向中间进行冒泡。具体来说,它先让最大的元素冒泡到序列的末尾,然后再让最小的元素冒泡到序列的开头,如此反复,直到序列完全有序。伪代码如下:
冒泡排序优化—双向冒泡优化 |
|
- 双向冒泡排序的例子:设原序列为{9,8,2,6,3,4,1,7},蓝色代表当前最小值,红色代表当前最大值,灰色代表已经排序后的数,4趟即可完成。
- 对比图如下:
从图中我们可以清楚地看到双向冒泡排序在处理大量数据时表现最佳,而有序性检查冒泡排序虽然稍微逊色一些,但仍然比普通冒泡排序要好。这说明不同的冒泡排序方法在处理大量数据时确实存在差异,这可能是因为它们在如何检查数据是否有序以及如何进行迭代方面有所不同。
(3)插入排序
算法原理:
- 第一个元素默认为已排序部分,从第二个元素开始处理。
- 在每一次外层循环的迭代中,将当前未排序部分的第一个元素(即 a[i])保存在临时变量temp中。
- 内层循环从当前未排序部分的位置向前遍历,直到找到 temp 应该插入的位置。如果a[j-1]小于temp,说明 temp 已经找到了应该插入的位置,否则将 a[j-1] 向后移动一位,为 temp 腾出位置。
- 将 temp 插入到已排序部分的正确位置,即 a[j] = tmp。
- 通过以上步骤,每一次外层循环迭代都会将未排序部分的一个元素插入到已排序部分的正确位置,使得已排序部分保持有序。
伪代码实现:
插入排序 |
|
时间复杂度分析:
- 外层循环执行了n-1次迭代。
- 内层循环在每次外层循环中最坏执行了i次,每次循环都需要遍历到数组的第一个元素。
- 总的时间复杂度表示:
- 可以看到T(n)的主要部分是n^2,所以插入排序的时间复杂度为O(n^2)。
- 在最优情况下,即输入数据已经完全有序的情况下,内层循环每次只需要比较一次就可以确定当前元素的位置,插入排序的时间复杂度为O(n),最坏情况则为O(n^2)。
实际性能与预测性能对比:可以发现实际性能与预测性能是接近一致的,两条曲线几乎完全吻合。
(4)合并排序
算法原理:
- 合并排序是一种基于分治策略的排序算法,它的主要思想是将待排序的数组分割成两个子序列,分别对这两个子序列进行排序,然后将排序好的子序列合并成一个有序的序列。
- 分割阶段: 首先将待排序的数组分割成两个子序列,直到每个子序列只包含一个元素为止。这个过程通过递归实现,将数组不断地分割成更小的子序列,直到每个子序列只有一个元素。
- 排序阶段: 对分割得到的子序列进行排序。这里采用递归的方式,对每个子序列分别调用合并排序算法,使得每个子序列都变成有序的。
- 合并阶段: 将排序好的子序列进行合并,生成最终的有序序列。合并的过程是通过比较两个有序子序列的元素,然后将较小的元素依次放入一个临时数组中,最后将临时数组中的元素复制回原始数组的相应位置,完成合并排序。
伪代码实现:
合并排序 |
|
时间复杂度分析:
- 分割阶段: 在每一次递归调用中,数组都被分成两半,直到每个子数组只有一个元素为止。假设数组长度为n,此数组会迭代logn层,那么分割阶段的时间复杂度可以用如下公式表示: