常見算法時間複雜度分析

当我们进行算法分析时,通常会忽略掉常数倍数的因子和低阶项,只考虑最高阶的项。这是因为在大规模问题下,较小的项和常数倍数的因子相对于最高阶的项来说变得可以忽略不计。

以下是一些常见的示例,说明了常数倍数的因子和高阶项对算法的影响:

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;
                }
            }
        }
    }

最坏时间复杂度是O(n^2)

即当输入的序列是降序排列时,每次比较都需要进行交换操作。在最坏情况下,共进行了(n-1)+(n-2)+…+2+1 = n*(n-1)/2次比较和交换,时间复杂度为O(n^2)

最好时间复杂度是O(n)

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;
    }
}

最好时间复杂度是O(nlogn)

快速排序的最好情况下,每次划分都能将待排序序列分成长度为 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)来说,它们之间的差异只是一个常数因子,而不会改变时间复杂度的增长趋势。

  • 深度算法:

最坏时间复杂度是O(n^2)

快速排序的最坏情况下,每次划分都将待排序序列分为长度为 1 和 n-1 的两个子序列,此时递归树的深度为 n,每层的时间复杂度为O(n),因此最坏情况下的时间复杂度为O(n^2)

归并排序

public class MergeSort {
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        int[] temp = new int[arr.length];
        mergeSort(arr, 0, arr.length - 1, temp);
    }

    private static void mergeSort(int[] arr, int left, int right, int[] temp) {
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(arr, left, mid, temp); // 左边归并排序,使得左子序列有序
            mergeSort(arr, mid + 1, right, temp); // 右边归并排序,使得右子序列有序
            merge(arr, left, mid, right, temp); // 将两个有序子数组合并操作
        }
    }

    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left; // 左序列指针
        int j = mid + 1; // 右序列指针
        int t = 0; // 临时数组指针
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }
        while (i <= mid) { // 将左边剩余元素填充进temp中
            temp[t++] = arr[i++];
        }
        while (j <= right) { // 将右序列剩余元素填充进temp中
            temp[t++] = arr[j++];
        }
        t = 0;
        // 将temp中的元素全部拷贝到原数组中
        while (left <= right) {
            arr[left++] = temp[t++];
        }
    }

    public static void main(String[] args) {
        int[] arr = {5, 3, 8, 6, 2, 7, 1, 4};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr)); // 输出排序结果:[1, 2, 3, 4, 5, 6, 7, 8]
    }
}

最坏/最坏的时间复杂度:O(nlogn)

最坏、最好情况下,归并排序的时间复杂度是O(nlogn)。这种情况发生在每次划分子问题时,都需要将待排序数组的元素分成两半,并且这个过程需要递归执行直到只剩下一个元素为止。
与快速排序的最好情况类似)然后再将这些子问题的解归并成一个有序数组。因此,在最坏、最好情况下,需要进行logn次划分,每次划分需要O(n)的时间复杂度来合并子问题的解。并且这个时间复杂度是稳定的

希尔排序

直接插入排序

具体的排序过程如下:

  1. 将第一个元素视为已排序的部分。
  2. 从第二个元素开始,逐个将待排序的元素插入到已排序的部分中。
  3. 对于待插入的元素,从已排序的部分的末尾开始比较,将比它大的元素依次后移,直到找到一个比它小的元素或者已经到达已排序部分的开头。然后将待插入的元素插入到这个位置。
  4. 重复步骤3,直到所有元素都插入完成。
  • 因此如果是原本倒序的元素,需要比较的次数最多。
假设要排序的数组为 [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]
public class InsertionSort {

    public static void main(String[] args) {
        int[] arr = {5, 2, 9, 1, 5, 6};
        insertionSort(arr);
    }

    // 插入排序函数
    public static void insertionSort(int[] arr) {
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int key = arr[i]; // 当前要插入的元素
            int j = i - 1;
            // 将大于key的元素向右移动,为key腾出位置
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key; // 插入key到正确的位置
        }
    }
}

最好时间复杂度是O(n)

最好情况发生在输入数据已经是有序的情况下。在这种情况下,插入排序只需要将每个元素与其前面的元素进行比较,不需要进行元素的移动,因此时间复杂度是线性的,即O(n)

最坏时间复杂度是O(n^2)

最坏情况发生在输入数据是逆序的情况下。在这种情况下,每个元素都必须与前面的所有元素进行比较,并且需要进行大量的元素移动,1+2+3+…+n = (n-1)*(n-1+1)/2 导致时间复杂度为O(n^2)

堆排序

public class HeapSort {

    public static void heapSort(int arr[]) {
        int n = arr.length;

        // 构建最大堆
        buildMaxHeap(arr, n);

        // 逐个提取元素并排序
        for (int i = n - 1; i > 0; i--) {
            // 交换堆顶元素(最大值)和当前未排序部分的最后一个元素
            swap(arr, 0, i);
            // 调整堆,使得未排序部分重新成为最大堆
            heapify(arr, i, 0);
        }
    }

    // 构建最大堆
    private static void buildMaxHeap(int arr[], int n) {
        // 从最后一个非叶子节点开始,逐个向上调整节点位置
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }
    }

    // 调整堆,使其满足最大堆性质
    private static void heapify(int arr[], int n, int i) {
        int largest = i; // 初始化最大值索引
        int left = 2 * i + 1; // 左子节点索引
        int right = 2 * i + 2; // 右子节点索引

        // 找到左子节点的索引,并比较其值与父节点的值
        if (left < n && arr[left] > arr[largest]) {
            largest = left;
        }

        // 找到右子节点的索引,并比较其值与父节点的值
        if (right < n && arr[right] > arr[largest]) {
            largest = right;
        }

        // 如果最大值索引不是当前节点索引,则交换节点值,并递归调整下层堆结构
        if (largest != i) {
            swap(arr, i, largest);
            heapify(arr, n, largest);
        }
    }

    // 交换数组中的两个元素
    private static void swap(int arr[], int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String args[]) {
        int arr[] = { 12, 11, 13, 5, 6, 7 };
        int n = arr.length;
        heapSort(arr);
    }
}

最佳时间复杂度:

O(n log n) - 堆排序的最佳时间复杂度也是O(n log n),因为它在构建最大堆和重复调整堆的过程中都需要对整个数组进行遍历,所以无论输入数据的分布如何,时间复杂度都保持不变。

平均时间复杂度:

O(n log n) - 平均情况下,堆排序的时间复杂度也是O(n log n)。虽然堆排序的性能不太依赖于输入数据的分布,但由于构建最大堆的操作会导致较多的比较和交换,因此其平均时间复杂度仍然是O(n log n)。

最坏时间复杂度:

O(n log n) - 堆排序的最坏时间复杂度是O(n log n),这是因为在最坏情况下,每次调整堆时都需要将元素下移到树的底部,这导致了较多的比较和交换操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
在进行排序算法的时间复杂度分析时,我们通常使用大O表示法来描述算法执行所需的时间。大O表示法是一种定性描述算法时间复杂度的方法。 对于希尔排序算法,它的时间复杂度介于O(n^1.3)到O(n^2)之间。具体的时间复杂度取决于所选取的增量序列和增量的取值。根据研究结果,如果增量序列的选择合理,希尔排序算法的时间复杂度约为O(n^1.3)。 对于其他排序算法的时间复杂度分析,我们可以通过比较基本操作的重复执行次数来进行。假设问题规模为n,解决该问题的算法中基本操作的执行次数为T(n)。如果存在一个辅助函数f(n),使得T(n)/f(n)的极限值为不等于零的常数,那么我们就可以说f(n)是T(n)的同数量级函数。因此,我们可以表示T(n) = O(f(n)),其中O(f(n))被称为算法的渐进时间复杂度,简称时间复杂度。时间复杂度越高,算法的执行效率越低。 举例来说,简单选择排序算法的最坏、最好和平均时间复杂度都为O(n^2),因此它是常见排序算法中性能最差的排序算法。在简单选择排序中,每一趟排序都需要选择出最小排序码的记录,需要进行n-i次比较,因此总的比较次数为∑i=1n−1(n−i)=n(n−1)/2=O(n^2)。 综上所述,排序算法的时间复杂度分析可以根据不同算法的特点和基本操作的重复执行次数进行。希尔排序算法的时间复杂度介于O(n^1.3)到O(n^2)之间,而简单选择排序算法的时间复杂度是O(n^2)。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [常见的排序算法及其复杂度分析](https://blog.csdn.net/Lyf_Ah/article/details/123796354)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [排序算法的时间复杂度](https://blog.csdn.net/Ehontoo/article/details/124274303)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

细水长流永不粹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值