图解算法数据结构——概述

算法复杂度

算法复杂度:时间复杂度,空间复杂度

时间复杂度

根据输入数据的特点,时间复杂度具有「最差」、「平均」、「最佳」三种情况,分别使用 O , Θ , Ω 三种符号表示。
大 O 是最常使用的时间复杂度评价渐进符号。
根据从小到大排列,常见的算法时间复杂度主要有:
O(1)<O(logN)<O(N)<O(NlogN)<O(N2)<O(2N)<O(N!)
在这里插入图片描述

常数 O(1):

运行次数与 N 大小呈常数关系,即不随输入数据大小 N 的变化而变化。

int algorithm(int N) {
    int a = 1;
    int b = 2;
    int x = a * b + N;
    return 1;
}

线性 O(N):

循环运行次数与 N 大小呈线性关系,时间复杂度为 O(N) 。

int algorithm(int N) {
    int count = 0;
    for (int i = 0; i < N; i++)
        count++;
    return count;
}

对于以下代码,虽然是两层循环,但第二层与 N 大小无关,因此整体仍与 N 呈线性关系。

int algorithm(int N) {
    int count = 0;
    int a = 10000;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < a; j++) {
            count++;
        }
    }
    return count;
}

平方 O(N2) :

两层循环相互独立,都与 N 呈线性关系,因此总体与 N 呈平方关系,时间复杂度为 O(N2)

int algorithm(int N) {
    int count = 0;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            count++;
        }
    }
    return count;
}

冒泡排序

int[] bubbleSort(int[] nums) {
    int N = nums.length;
    for (int i = 0; i < N - 1; i++) {
        for (int j = 0; j < N - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                int tmp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = tmp;
            }
        }
    }
    return nums;
}

指数 O(2N):

算法中,指数阶常出现于递归,算法原理图与代码如下所示。

int algorithm(int N) {
    if (N <= 0) return 1;
    int count_1 = algorithm(N - 1);
    int count_2 = algorithm(N - 1);
    return count_1 + count_2;
}

在这里插入图片描述

阶乘 O(N!) :

如下图与代码所示,阶乘常使用递归实现,算法原理:第一层分裂出 N 个,第二层分裂出 N - 1个,…… ,直至到第 N 层时终止并回溯。

int algorithm(int N) {
    if (N <= 0) return 1;
    int count = 0;
    for (int i = 0; i < N; i++) {
        count += algorithm(N - 1);
    }
    return count;
}

在这里插入图片描述

对数 O(log N) :

对数阶与指数阶相反,指数阶为 “每轮分裂出两倍的情况” ,而对数阶是 “每轮排除一半的情况” 。对数阶常出现于「二分法」、「分治」等算法中,体现着 “一分为二” 或 “一分为多” 的算法思想。
设循环次数为 m ,则输入数据大小 N 与 2m呈线性关系,两边同时取log2 对数,则得到循环次数 m 与 log2N呈线性关系,即时间复杂度为 O(logN)

int algorithm(int N) {
    int count = 0;
    float i = N;
    while (i > 1) {
        i = i / 2;
        count++;
    }
    return count;
}

如下图所示,为二分查找的时间复杂度示意图,每次二分将搜索区间缩小一半。
在这里插入图片描述

线性对数O(NlogN) :

两层循环相互独立,第一层和第二层时间复杂度分别为O(logN) 和O(N) ,则总体时间复杂度为O(NlogN) ;

int algorithm(int N) {
    int count = 0;
    float i = N;
    while (i > 1) {
        i = i / 2;
        for (int j = 0; j < N; j++)
            count++;
    }
    return count;
}

线性对数阶常出现于排序算法,例如「快速排序」、「归并排序」、「堆排序」等,其时间复杂度原理如下图所示。
在这里插入图片描述

空间复杂度

  • 输入空间: 存储输入数据所需的空间大小;
  • 暂存空间: 算法运行过程中,存储所有中间变量和对象等数据所需的空间大小;
  • 输出空间: 算法运行返回时,存储输出数据所需的空间大小;
    通常情况下,空间复杂度指在输入数据大小为 N 时,算法运行所使用的「暂存空间」+「输出空间」的总体大小。

而根据不同来源,算法使用的内存空间分为三类:
指令空间: 编译后,程序指令所使用的内存空间。
数据空间: 算法中的各项变量使用的空间,包括:声明的常量、变量、动态数组、动态对象等使用的内存空间。
栈帧空间: 程序调用函数是基于栈实现的,函数在调用期间,占用常量大小的栈帧空间,直至返回后释放。如以下代码所示,在循环中调用函数,每轮调用 test() 返回后,栈帧空间已被释放,因此空间复杂度仍为 O(1)

int test() {
    return 0;
}

void algorithm(int N) {
    for (int i = 0; i < N; i++) {
        test();
    }
}

算法中,栈帧空间的累计常出现于递归调用。如以下代码所示,通过递归调用,会同时存在 N 个未返回的函数 algorithm() ,此时累计使用 O(N) 大小的栈帧空间。

int algorithm(int N) {
    if (N <= 1) return 1;
    return algorithm(N - 1) + 1;
}

通常情况下,空间复杂度统计算法在 “最差情况” 下使用的空间大小,以体现算法运行所需预留的空间量,使用符号 O表示。
最差情况有两层含义,分别为「最差输入数据」、算法运行中的「最差运行点」。例如以下代码:

  • 最差输入数据: 当 N≤10 时,数组 nums 的长度恒定为 10 ,空间复杂度为 O(10)=O(1);当 N > 10时,数组 nums长度为 N,空间复杂度为 O(N);因此,空间复杂度应为最差输入数据情况下的 O(N) 。
  • 最差运行点: 在执行 nums = [0] * 10 时,算法仅使用 O(1) 大小的空间;而当执行 nums = [0] * N 时,算法使用 O(N) 的空间;因此,空间复杂度应为最差运行点的 O(N) 。
    根据从小到大排列,常见的算法空间复杂度有:
    O(1)<O(logN)<O(N)<O(N2)<O(2N)

常数 O(1):

普通常量、变量、对象、元素数量与输入数据大小 NN 无关的集合,皆使用常数大小的空间。

void algorithm(int N) {
    int num = 0;
    int[] nums = new int[10000];
    Node node = new Node(0);
    Map<Integer, String> dic = new HashMap<>() {{ put(0, "0"); }};
}

如以下代码所示,虽然函数 test() 调用了 NN 次,但每轮调用后 test() 已返回,无累计栈帧空间使用,因此空间复杂度仍为 O(1)。

void algorithm(int N) {
    for (int i = 0; i < N; i++) {
        test();
    }
}

线性 O(N):

元素数量与 N 呈线性关系的任意类型集合(常见于一维数组、链表、哈希表等),皆使用线性大小的空间。

void algorithm(int N) {
    int[] nums_1 = new int[N];
    int[] nums_2 = new int[N / 2];

    List<Node> nodes = new ArrayList<>();
    for (int i = 0; i < N; i++) {
        nodes.add(new Node(i));
    }

    Map<Integer, String> dic = new HashMap<>();
    for (int i = 0; i < N; i++) {
        dic.put(i, String.valueOf(i));
    }
}

如下图与代码所示,此递归调用期间,会同时存在 N 个未返回的 algorithm() 函数,因此使用 O(N)大小的栈帧空间。

int algorithm(int N) {
    if (N <= 1) return 1;
    return algorithm(N - 1) + 1;
}

平方 O(N2)

元素数量与 NN 呈平方关系的任意类型集合(常见于矩阵),皆使用平方大小的空间。

void algorithm(int N) {
    int num_matrix[][] = new int[N][N];

    List<List<Node>> node_matrix = new ArrayList<>();
    for (int i = 0; i < N; i++) {
        List<Node> nodes = new ArrayList<>();
        for (int j = 0; j < N; j++) {
            nodes.add(new Node(j));
        }
        node_matrix.add(nodes);
    }
}

如下图与代码所示,递归调用时同时存在 N 个未返回的 algorithm() 函数,使用 O(N)栈帧空间;每层递归函数中声明了数组,平均长度为 N 2 \frac{N}{2} 2N,使用 O(N) 空间;因此总体空间复杂度为 O(N2)

int algorithm(int N) {
    if (N <= 0) return 0;
    int[] nums = new int[N];
    return algorithm(N - 1);
}

指数 O(2N)

指数阶常见于二叉树、多叉树。例如,高度为 NN 的「满二叉树」的节点数量为 2N ,占用 O(2N)大小的空间;同理,高度为 N 的「满 m 叉树」的节点数量为 mN,占用 O(mN) = O( 2N ) 大小的空间。
在这里插入图片描述

对数 O(logN) :

对数阶常出现于分治算法的栈帧空间累计、数据类型转换等,例如:

  • 快速排序 ,平均空间复杂度为 Θ ( log ⁡ N ) \Theta(\log N) Θ(logN) ,最差空间复杂度为 O(N)。拓展知识:通过应用 Tail Call Optimization ,可以将快速排序的最差空间复杂度限定至 O(N)。
  • 数字转化为字符串 ,设某正整数为 N ,则字符串的空间复杂度为O(logN) 。推导如下:正整数 N 的位数为 l o g 10 N log_{10} N log10N 即转化的字符串长度为 log ⁡ 10 N \log_{10} N log10N,因此空间复杂度为 O(logN) 。

时空权衡

对于算法的性能,需要从时间和空间的使用情况来综合评价。优良的算法应具备两个特性,即时间和空间复杂度皆较低。而实际上,对于某个算法问题,同时优化时间复杂度和空间复杂度是非常困难的。降低时间复杂度,往往是以提升空间复杂度为代价的,反之亦然。由于当代计算机的内存充足,通常情况下,算法设计中一般会采取「空间换时间」的做法,即牺牲部分计算机存储空间,来提升算法的运行速度。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值