时间复杂度和空间复杂度

一、算法的时间复杂度

1.1 定义

     衡量算法执行时间随着输入数据量增加而增加的速度。它通常用大O符号(O)表示,形式如O(n), O(n^{2}), O(\log n)等,其中n代表输入数据的规模。

1.2 渐进分析

时间复杂度关注的是当输入数据量趋向于无穷大时,算法执行时间的增长趋势。因此,它忽略了常数因子和低阶项,专注于最高阶项,通过高数的极限思想可知,当n趋向于无穷大时,常熟因子和低阶项可忽略不计。

1.3 表示方法

O(f(n))表示算法执行的基本操作次数上限为(f(n))乘以某个常数。例如,如果一个算法的时间复杂度是O(n^{2}),意味着算法的操作次数大致上不会超过(Cn^{2})次,其中(C)是一个正的常数。

1.4 常见的时间复杂度表达式类别

O(1):常数时间复杂度,表示算法的执行时间不随输入数据规模改变。

O(\log n):对数时间复杂度,常见于二分查找等高效算法。

O(n):线性时间复杂度,每个元素被处理一次。

O(n\log n):常见于高效的排序算法,如快速排序、合并排序。

O(n^{2}):平方时间复杂度,例如冒泡排序、选择排序。 

O(n^{3}), O(2^{n}),O(n!):分别代表立方时间复杂度、指数时间复杂度和阶乘时间复杂度,这些通常表示算法效率较低。

当n趋向于无穷大时,这些时间复杂度函数的增长速度按照以下顺序排列:

1.O(1):
常数时间复杂度,不会随着n的增加而改变,时间复杂度最低
2.O(\log n):
对数函数增长非常缓慢,是所有这些函数中增长最慢的。
3.O(n):
线性时间复杂度比对数函数增长快,但仍然远远低于其他更高阶的多项式和指数函数。
4.O(n\log n):
这个时间复杂度介于线性和平方之间,通常在算法设计中是一个很好的折衷,例如在排序算法中。
5.O(n^{2}):
平方级别的复杂度比线性时间复杂度增长快得多,但仍然属于多项式级别。
6.O(n^{3}):
高阶多项式,但仍然比指数函数慢。
7.O(2^{n}):
指数函数开始占据主导地位,增长速度远超过多项式。即使是低阶指数函数,如2^{n},也比高阶多项式更快增长。
8.O(n!):
阶乘函数的增长速度是最快的,尤其是当n变得很大时,n!的值远远超过任何n的幂。阶乘函数的增长速度远超指数函数,是所有列出的函数中增长最快的。
总结来说,随着n趋向于无穷大,这些函数的增长速度从慢到快依次是:

O(1)<<O(\log n) < O(n) < O(n\log n) < O(n^{2}) < O(n^{3}) < O(2^{n}) << O(n!)。

在算法设计中,通常优先选择低阶时间复杂度的算法,以确保在处理大规模数据时的效率。

时间复杂度的计算主要是通过分析算法中基本操作的执行次数与输入数据规模(n)之间的关系来完成的。下面通过几组java代码示例来分别展示以上常见的时间复杂度表达式,加深理解。

O(1) 时间复杂度代码示例 

public class ConstantTimeExample {
    public static void main(String[] args) {
        // 初始化一个数组
        int[] numbers = {10, 20, 30, 40, 50};
        
        // 访问数组中的第三个元素(索引为2)
        int thirdElement = numbers[2];
        
        // 打印该元素
        System.out.println("The third element is: " + thirdElement);
    }
}

分析:以上代码在一个给定数组中访问固定索引位置的元素,无论数组有多大,根据数组的数据结构特性知道,可以通过确定的索引来立即访问到任意索引位置的元素,时间复杂度均为O(1),因为只需要操作一次。
 

O(\log n) 时间复杂度代码示例 

这里以经典的二分查找算法为例

public class BinarySearchExample {
    public static int binarySearch(Integer[] arr, int target) {
        int left = 0;
        int right = arr.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2; // 防止(left + right)溢出
            if (arr[mid] == target) {
                return mid; // 找到目标值,返回其索引
            } else if (arr[mid] < target) {
                left = mid + 1; // 调整左边界
            } else {
                right = mid - 1; // 调整右边界
            }
        }

        return -1; // 没有找到目标值,返回-1
    }

    public static void main(String[] args) {
        Integer[] sortedArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int targetValue = 7;
        int result = binarySearch(sortedArray, targetValue);
        if (result != -1) {
            System.out.println("Element found at index: " + result);
        } else {
            System.out.println("Element not found in the array.");
        }
    }
}

元素所在索引位置为6

分析:给定一个数组和待查找的目标值,每次循环都找先把数组一分为二,把中间值和目标值比较

如果中间值等于目标值直接返回,

中间值大于目标值,则说明目标值在左边界索引和中间值索引之间,把右边界索引调整为中间值索引减一的索引(因为小于中间值,所以不包含中间值索引)

中间值小于目标值,说明目标值在中间值索引和右边界索引之间,把左边界索引调整为中间值索引加一的索引(因为大于中间值,所以不包含中间值索引)

那么二分查找的时间复杂度到底怎么算出来的?

二分搜索最坏的情况就是折半一直找到最后一个元素,首先观察规律

开始时,是从n个元素中查找

第一次折半时,是从\frac{n}{2}个元素中查找

第二次折半时,是从\frac{n}{4}个元素中查找

假设第k次折半后只剩一个元素,即是从\frac{n}{2^k}个元素中查找

\frac{n}{2^k} = 1,即 n = ^{2^k},由对数定义知道 k = \log_{2}n,在计算机科学中如果没有特殊说明,\log n默认就是以2为底,即k=\log n

即操作k次才能找到最后一个元素,所以时间复杂度为O(\log n)

O(n)时间复杂度代码示例:

计算数组元素的和

public int sumOfArray(int[] nums) {
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    return sum;
}

查找数组中指定元素的索引

public int findIndexOf(int[] nums, int target) {
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] == target) {
            return i;
        }
    }
    return -1; // 如果未找到,返回-1
}

删除链表中的重复节点

class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public ListNode deleteDuplicates(ListNode head) {
    ListNode current = head;
    while (current != null && current.next != null) {
        if (current.val == current.next.val) {
            current.next = current.next.next;
        } else {
            current = current.next;
        }
    }
    return head;
}

这些示例都具有O(n)的时间复杂度,因为每个算法的核心循环或操作都直接依赖于输入数据的大小n,即数据n多大,基本就需要循环多少次。
 

O(n\log n)时间复杂度代码示例

import java.util.Arrays;
import java.util.Comparator;
import java.util.stream.IntStream;

public class SimpleQuickSort {

    public static void quickSort(int[] arr) {
       quickSortHelper(arr, 0, arr.length - 1);
    }

    private static void quickSortHelper(int[] arr, int low, int high) {
        if (low < high) {
            int pivotIndex = partition(arr, low, high);
            quickSortHelper(arr, low, pivotIndex - 1);
            quickSortHelper(arr, pivotIndex + 1, high);
        }
    }

    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[high];
        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;
    }

    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 = {10, 7, 8, 9, 1, 5};
        System.out.println("Original array: " + Arrays.toString(arr));
        quickSort(arr);
        System.out.println("Sorted array: " + Arrays.toString(arr));
    }
}

以上代码是快速排序的一个递归实现

影响分析上面代码的时间复杂度的函数是quickSortHelperpartition

quickSortHelper方法采用了递归调用,最好情况下每次调用都能减少一半规模,类似于二分查找的时间复杂度计算方法,为O(\log n)

partition方法内部主要是采用了for循环,遍历整个数组,复杂度为O(n)

二者结合的时间复杂度为O(n\log n)

O(n^{2})时间复杂度代码示例

import java.util.HashMap;
import java.util.Map;

public class PairSums {

    public static void countPairSums(int[] arr) {
        int n = arr.length;
        Map<Integer, Integer> sumCounts = new HashMap<>();

        // 双重循环遍历数组中的每对元素
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                // 计算当前元素对的和
                int pairSum = arr[i] + arr[j];
                // 更新该和在map中的计数
                sumCounts.put(pairSum, sumCounts.getOrDefault(pairSum, 0) + 1);
            }
        }

        // 打印每种和及其出现的次数
        for (Map.Entry<Integer, Integer> entry : sumCounts.entrySet()) {
            System.out.println("Sum: " + entry.getKey() + ", Count: " + entry.getValue());
        }
    }

    public static void main(String[] args) {
        int[] arr = {1, 5, 7, -1, 5};
        countPairSums(arr);
    }
}

以上代码计算一个数组中所有两两元素对的和,并统计这些和的出现次数

这段代码中,外层循环遍历数组的每个元素,内层循环则与外层循环的元素配对,计算它们的和,并使用哈希表统计所有不同和值的出现次数。由于存在两层循环,每层循环都依赖于数组长度n,因此总的时间复杂度为O(n^{2})。
 

O(n^{3}), O(2^{n}),O(n!)这三种时间复杂度较高,实际场景一般很少直接使用,都会寻找代替算法或者特定场景的优化算法,这里不做展示了

二、空间复杂度

空间复杂度用来衡量一个算法在执行过程中临时占用存储空间大小的量度。以下是一些常见的空间复杂度类别:

O(1) - 常数空间:
算法所需的存储空间不随输入数据大小的变化而变化,是固定大小的。例如,基本的数学运算或简单的变量赋值。

public class ConstantSpaceExample {

    // 方法用于计算两个整数的和
    public static int addTwoNumbers(int a, int b) {
        int sum = a + b; // 使用固定数量的变量来存储结果
        return sum;
    }

    public static void main(String[] args) {
        // 示例调用
        int result = addTwoNumbers(100, 200);
        System.out.println("两数之和为: " + result);
    }
}

分析:在这个例子中,无论是a、b还是sum变量,都是固定使用的,不会因为输入参数的大小而增加额外的存储需求,因此该函数的空间复杂度为O(1)。

O(\log n) - 对数空间:
空间需求与输入数据大小的对数成正比。例如,二分查找或平衡二叉树的遍历。

public class BinarySearch {
    
    // 二分查找函数
    public static int binarySearch(int[] sortedArray, int target) {
        int left = 0;
        int right = sortedArray.length - 1;
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            
            // 如果目标值等于中间元素,返回索引
            if (sortedArray[mid] == target) {
                return mid;
            } else if (sortedArray[mid] < target) {
                // 目标值大于中间元素,更新左边界
                left = mid + 1;
            } else {
                // 目标值小于中间元素,更新右边界
                right = mid - 1;
            }
        }
        
        // 如果未找到目标值,返回-1
        return -1;
    }

    public static void main(String[] args) {
        int[] array = {1, 3, 5, 7, 9};
        int target = 5;
        int index = binarySearch(array, target);
        if (index != -1) {
            System.out.println("目标值 " + target + " 在数组中的索引是: " + index);
        } else {
            System.out.println("目标值 " + target + " 不在数组中");
        }
    }
}

分析:二分查找算法在每一步都将搜索范围减半,因此递归深度最多为\log n,其中n是数组的大小。由于递归调用栈的深度与\log n成正比,所以二分查找的空间复杂度是O(\log n)。

O(n) - 线性空间:
空间需求与输入数据大小成正比。例如,创建一个与输入数组大小相同的辅助数组。

public class LinearSpaceExample {

    // 创建新数组并复制原数组内容
    public static int[] copyArray(int[] originalArray) {
        int n = originalArray.length;
        int[] copiedArray = new int[n]; // 新数组的大小与原数组相同

        // 遍历原数组并将元素复制到新数组
        for (int i = 0; i < n; i++) {
            copiedArray[i] = originalArray[i];
        }

        return copiedArray;
    }

    public static void main(String[] args) {
        int[] original = {1, 2, 3, 4, 5};
        int[] copied = copyArray(original);

        System.out.println("Original Array: " + Arrays.toString(original));
        System.out.println("Copied Array: " + Arrays.toString(copied));
    }
}

分析:在这个例子中,copiedArray的大小与originalArray相同,因此所需的额外空间与输入数组的长度成正比,即O(n)。

O(n\log n) - 线性对数空间:
空间需求与输入数据的线性对数成正比。例如,归并排序的辅助数组。

O(n^{2}) - 平方空间:
空间需求与输入数据的平方成正比。例如,二维数组或在一些算法中创建的辅助矩阵。

O(n^{k}) - 高阶多项式空间:
空间需求与输入数据的k次方成正比,k > 1。较少见,但在某些算法中可能出现。

O(2^{n}) - 指数空间:
空间需求与输入数据的指数成正比。例如,递归深度特别深的情况。

O(n!) - 阶乘空间:
极少情况下,空间需求与输入数据的阶乘成正比,通常发生在极端的组合问题中。

空间复杂度分析通常关注算法运行时临时存储的需求,不包括输入数据本身占用的空间。在实际应用中,优化空间复杂度通常是减少算法运行时内存消耗的关键。

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值