当我们进行算法分析时,通常会忽略掉常数倍数的因子和低阶项,只考虑最高阶的项。这是因为在大规模问题下,较小的项和常数倍数的因子相对于最高阶的项来说变得可以忽略不计。
以下是一些常见的示例,说明了常数倍数的因子和高阶项对算法的影响:
O(2n) 和 O(n):在 O(2n) 中,常数倍数因子为 2,而在 O(n) 中为 1。但是,当 n 变得非常大时,2n 和 n 之间的差距就变得微不足道,因此我们可以说 O(2n) 等价于 O(n)
O(3n^2) 和 O(n^2):在 O(3n^2) 中,常数倍数因子为 3,而在 O(n^2) 中为 1。但是,当 n 变得非常大时,3n^2 和 n^2 之间的差距就变得微不足道,因此我们可以说 O(3n^2) 等价于 O(n^2)
O(n^2 + n) 和 O(n^2):在 O(n^2 + n) 中,我们有两个项,分别是 n^2 和 n。然而,在大规模问题下,n 这样的低阶项可以被 n^2 这样的高阶项主导,因此我们可以忽略掉 n,即 O(n^2 + n) 等价于 O(n^2)
O(n^3 + n^2) 和 O(n^3):在 O(n^3 + n^2) 中,我们有两个项,分别是 n^3 和 n^2。同样,在大规模问题下,n^2 这样的低阶项可以被 n^3 这样的高阶项主导,因此我们可以忽略掉 n^2,即 O(n^3 + n^2) 等价于 O(n^3)
通过忽略常数倍数的因子和低阶项,我们可以简化算法的复杂度表示,并更好地理解算法的增长趋势和相对性能。这种简化使得我们能够更容易地比较和分析不同算法之间的效率。
基本的冒泡排序算法
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换arr[j]和arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
即当输入的序列是降序排列时,每次比较都需要进行交换操作。在最坏情况下,共进行了(n-1)+(n-2)+…+2+1 = n*(n-1)/2次比较和交换,时间复杂度为O(n^2)
1、在最好的情况下,即当输入的序列已经是升序排列时,冒泡排序只需要进行一遍比较即可完成排序。
2、但是根据上面代码,无论输入序列是否有序,冒泡排序都将进行n*(n-1)/2次比较,时间复杂度为O(n^2)。
3、其实,最好情况下的时间复杂度O(n)通常是指在某些优化的冒泡排序算法中,如果发现某一轮比较中没有交换操作,就可以提前结束排序。
/**
* 改进版冒泡排序
* @param arr
*/
public static void improvedBubbleSort(int[] arr) {
int n = arr.length;
boolean swapped;
for (int i = 0; i < n - 1; i++) {
swapped = false;
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换arr[j]和arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
// 如果某轮比较没有发生交换,说明已经有序,提前结束排序
if (!swapped) {
break;
}
}
}
public class QuickSort {
public static void main(String[] args) {
int[] arr = {7, 2, 1, 6, 8, 5, 3, 4};
quickSort(arr, 0, arr.length - 1);
for (int num : arr) {
System.out.print(num + " ");
}
}
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 分区操作,将数组分为两部分,返回基准元素的索引
int pivot = partition(arr, low, high);
// 对左子数组进行快速排序
quickSort(arr, low, pivot - 1);
// 对右子数组进行快速排序
quickSort(arr, pivot + 1, high);
}
}
public static int partition(int[] arr, int low, int high) {
// 选择最后一个元素作为基准
int pivot = arr[high];
// i 指向小于基准的元素的位置
int i = low - 1;
// 遍历数组,将小于基准的元素移动到基准的左边
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
swap(arr, i, j);
}
}
// 将基准元素放到正确的位置上
swap(arr, i + 1, high);
// 返回基准元素的索引
return i + 1;
}
public static void swap(int[] arr, int i, int j) {
// 交换数组中两个元素的位置
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
快速排序的最好情况下,每次划分都能将待排序序列分成长度为 n/2 的两个子序列。假设递归树的深度为 d,初始时,序列的长度为 n。每次划分后,序列的长度变为原来的一半,即 n/2。那么经过 d 次划分后,序列的长度变为 n/(2^d)。当划分完毕后,序列的长度为 1。
所以我们有以下等式:n/(2^d) = 1
通过移项,可以得到:
n = 2^d
取以 2 为底的对数,我们得到:
d = log2(n)
此时递归树的深度为 log2n,每层的时间复杂度为O(n),因此最好情况下的时间复杂度为O(nlogn)
在算法分析中,我们通常只关注时间复杂度的增长趋势,而不是具体的常数因子或底数。因此,在常见的情况下,会省略对数的底数,并将时间复杂度简化为O(n log n)。
对数的底数对于增长趋势的影响较小。对于底数为2的对数(log2n)和底数为10的对数(log10n)来说,它们之间的差异只是一个常数因子,而不会改变时间复杂度的增长趋势。
深度算法:
快速排序的最坏情况下,每次划分都将待排序序列分为长度为 1 和 n-1 的两个子序列,此时递归树的深度为 n,每层的时间复杂度为O(n),因此最坏情况下的时间复杂度为O(n^2)
具体的排序过程如下:
假设要排序的数组为 [5, 2, 4, 6, 1, 3]
第一步,将第一个元素 5 视为已排序的部分:[5]
第二步,将第二个元素 2 插入到已排序的部分中,比较得到 [2, 5]
第三步,将第三个元素 4 插入到已排序的部分中,比较得到 [2, 4, 5]
第四步,将第四个元素 6 插入到已排序的部分中,比较得到 [2, 4, 5, 6]
第五步,将第五个元素 1 插入到已排序的部分中,比较得到 [1, 2, 4, 5, 6]
第六步,将第六个元素 3 插入到已排序的部分中,比较得到 [1, 2, 3, 4, 5, 6]
最终得到的排序结果为 [1, 2, 3, 4, 5, 6]