算法性能分析【时间复杂度、空间复杂度】

阅读建议:
1. 前面的概念部分都是很重要的知识点!一定要掌握!
2. 后面的例题讲解也必须要看的,不然很可能以为自己会了实际还是懵懵懂懂,最好跟着我一步步分析,而且每一处地方都尽量先独立思考,再看分析。

一般衡量不同算法之间的优劣,主要从算法所占用的「时间」和「空间」两个维度去考量。

  • 时间维度:指当前算法运行所消耗的时间,我们通常用「时间复杂度」描述。
  • 空间维度:指当前算法运行过程中需要占用的内存空间大小,我们通常用「空间复杂度」描述。

一、时间复杂度

概念

时间复杂度是一个定性描述算法运行时间的函数。

假设算法的问题规模为n,操作单元数量用函数f(n)表示,随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这便是时间复杂度,记为 O(f(n))。

O( )

  1. 大O用来表示上界的,用它作为算法最坏情况运行时间的上界。
  2. 大O是数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度,数据量的常数项系数不起决定性作用。
  3. 如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。

但是对于大常数如10^7 ,10^9 等是需要考虑的!

1.

都知道快速排序是O(nlogn),但是当数据已经有序情况下,快速排序的时间复杂度是O(n^2) 的,所以严格从大O的定义来讲,快速排序的时间复杂度应该是O(n^2)但是我们依然说快速排序是O(nlogn)的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界

面试中问的算法的时间复杂度,指的都是一般情况

2.

我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于此给出的算法时间复杂的排行如下:

O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(nlogn)线性对数阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(2^n)指数阶

例题讲解

例1

题目描述:找出n个字符串中相同的两个字符串(假设这里只有两个相同的字符串)。问:暴力枚举的时间复杂度?优化解法的时间复杂度?

暴力枚举的时间复杂度?
  1. 每个字符串要和另外的n-1个字符串比较!其时间复杂度O(n2) .
    • 比较过程中要遍历字符串以作内部的比较,O(m)

因此,算法总的时间复杂度为O(n2 * m)

//【算法伪代码如下】
for (每个字符串str1 : 含有n个字符串的字符串数组) { //O(n)
    string s = str1;//从n个字符串中拿出一个字符串
    for (每个字符串str2 : 不包括s的含有n-1个字符串的字符串数组) { //依次与n-1个字符串作比较,O(n)
        //开始比较str1和str2这两个字符串, O(m)
        //因为要考虑的是最坏情况下嘛,最坏情况下str1和str2长度相等且前面的元素都相等,至结尾时才能知道等不等
        //因为比较字符串相等是要字符对应位置都要一一对应相等,因此不是写两个for嵌套!
        for (int i = 0, j = 0; i < str1.size(), j < str2.size(); ++i, ++j) { //O(m)
            if (str[1] != str2[j]) break;
        }
    }
}
优化解法的时间复杂度?
优化解法1. 排序后比较

算法思路:对n个字符串按字典顺序排序,排序后n个字符串就是有序的,即两个相同的字符串是挨在一起的。因此我们只需要比较相邻的字符串是否相同,如果相同说明找到了两个相同的两个字符串,返回。

算法步骤:

  1. 先排序:快速排序时间复杂度为O(nlogn),由于字符串的长度是m,那么快速排序每次的比较都要有m次的字符比较的操作,就是O(m*nlog n) 。

    • 再详细解释一下:我们知道给整型数组中的元素快排时间复杂度为O(nlogn),字符串不同于整型数字,它有多个字符,因此最坏情况下,所有字符串都有相同的长度(m),字符串要比较到末尾字符,也就是还需要O(m)

  2. 再遍历,比较相邻字符串,O(n)

    • 比较字符串,O(m)

    因此步骤2的总时间复杂度为O(n * m)

所以总的时间复杂度是 O(m*nlogn + m*n) ==>把m*n提取出来变成 O(m*n*(logn+1)),再省略常数项最后的时间复杂度是 O(m*nlogn)

	// 1.对字符串数组进行快排, O(m*nlogn)
	sort(strings);
	// 2.比较相邻的字符串是否相同
	for (int i = 0; i < strings.size(); ++i) //O(n)
	    if (strings[i] == strings[i-1]){ //比较两个字符串,最坏情况下两字符串长度相等且前面的元素都相等,O(m)
	        找到了相同的字符串
	     }
     }
优化解法2. 哈希

算法思路:

算法步骤:

  1. 构建哈希表,O(n*m)
  2. 哈希查找相同哈希值的字符串,O(1)

因此总的时间复杂度是 O(n*m).

例2

lc151. 反转字符串中的单词

题意

已知 s 中每个 单词 都至少由一个空格分隔且s 中 至少存在一个单词,返回单词顺序颠倒且单词间用单个空格连接后的字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中不应包含任何额外的空格。

输入:s = "  hello world  "
输出:"world hello"
法一. 使用额外空间

🔻 steps:

  1. 逆序遍历s,将每个单词拷贝到s_cp中
  2. 删掉最后多余的空格
代码
string reverseWords(string s) {
    string s_cp;
    int sublen = 0;
    for (int i = s.size() - 1; i >= 0; --i) {
        while (i >= 0 && s[i] == ' ') --i;
        while (i >= 0 && s[i] != ' ') {
            ++sublen;
            --i;
        } //while结束后i指向空格
        if (sublen) s_cp += s.substr(i + 1, sublen) + " "; //O(1)
        sublen = 0;
    }
    return s_cp.substr(0, s_cp.size() - 1);//删掉最后多余的空格
}
复杂度分析
  • 时间复杂度:O(n)。

    • 外层循环遍历字符串是O(n)
    • 循环内,截取单词的substr的时间复杂度与截取长度成线性关系,设最长的单词长度为maxlen,则substr的时间复杂度就是O(maxlen),因为截取的字符串都只是"一个单词",其长度只是一个较小的常数,因此这里substr截取字符串的时间复杂度就是O(1)
    • 综上,总的时间复杂度就是O(1)
  • 空间复杂度:O(n)

法二. O(1)原地修改

🔻 steps:

  1. 移除多余空格(同lc.27法一:快慢指针同向,覆盖移除)。但还要考虑前置空格
  2. 将整个字符串反转–左右指针
  3. 将每个单词反转
代码
string reverseWords(string s) {
    //(1)移除多余空格(同lc.27法一:快慢指针同向,覆盖移除)。但还要考虑前置空格
    int sl = 0;
    for (int f = 0; f < s.size(); ++f) {
        if (s[f] != ' ') {
            //当f遇到非' '字符,先要给上一个单词的末尾添上空格,再开始拷贝覆盖下一个单词
            if (sl != 0) s[sl++] = ' ';
            while (f < s.size() && s[f] != ' ') s[sl++] = s[f++];//将s[f]拷贝覆盖到sl
        }
    }
    s.resize(sl);
    //(2)将整个字符串反转--左右指针
    for (int l = 0, r = s.size() - 1; l < r ; ++l, --r) swap(s[l],s[r]);
    //(3)将每个单词反转
    int tmp = 0;
    for (int slow = 0, fast = 0; fast < s.size(); slow = tmp, fast = tmp) {
        // cout << "the first character of word to be reversed: " << s[tmp] << endl;
        while (fast < s.size() && s[fast] != ' ') ++fast;//退出时fast指向单词间的空格
        tmp = fast-- + 1;//保存下个单词的起始位置,并让fast左移指向待反转单词的最后一个字符
        // cout << "start swap!swapRange:["<< s[slow] << "<=>" << s[fast] << "]" << endl;
        while (slow < fast) swap(s[slow++],s[fast--]);//反转单词
    }
    return s;
}
复杂度分析
  • 时间复杂度:O(n)

    经过前面所有的时间复杂度分析,到这里你应该就知道了这里为什么是O(n)了。其实就是前面说的:

    如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。

  • 空间复杂度:O(1)

二、空间复杂度

空间复杂度指的是一个算法在运行过程中临时占用内存空间大小的量度,记做S(n)或O(f(n))。

利用程序的空间复杂度,可以对程序运行中需要多少内存有个预先估计。

  • 空间复杂度为O(1):所需开辟的内存空间并不随着问题规模 n的变化而变化,即算法空间复杂度为一个常量

  • 空间复杂度为O(n):占用空间 和 问题规模 n 成线性关系.

  • 空间复杂度O(n2),O(n3)····:和时间复杂度其实都差不多同理,没什么特殊的

  • 空间复杂度O(logn):在递归的时候,会出现空间复杂度为logn的情况。

另外需注意!刷算法题时说的空间复杂度,不包含用来存储返回值的空间!!!

之后会单独开一篇专门讲解递归算法的性能分析,包括时间复杂度和空间复杂度的分析。

如果对你有帮助的话那就太好啦!🤗

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值