数据结构之时间复杂度与空间复杂度

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之时间复杂度与空间复杂度相关的知识,希望对你有一定的参考价值。

数据结构

数据结构中有简单存储数据的存储结构,比如顺序表、链表、串。不仅仅要存储数据、存起来还要查找数据,比如搜索树、哈希表等等这样结构。在写通讯录中,存储联系人信息我们可以用顺序表(数组)存储或者链表。

什么是数据结构?

数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。

什么是算法?

算法是指解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。通过某种处理得到结果。数据结构和算法是不分家,数据结构中包含一些算法,一些算法的解决又离不开数据结构

如何学好数据结构呢?

  • 勤加练习,多敲代码

时时刻刻敲代码,你就成功学好了。哈哈哈哈,开个玩笑,反正需要勤加练习多做题

  • 多画图,多分析

数据结构的逻辑思想十分重要,所以我们需要多画图来便于我们理解与分析。

推荐的书籍与做题地方:《剑指offer》、leetcode

时间复杂度和空间复杂度

算法效率

如何衡量一个算法的好坏?

复杂度计算:时间效率和空间效率,算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。

在算法效率这一方面我们只需要重点关注时间复杂度。

硬件发展:摩尔定律

摩尔定律是英特尔创始人之一戈登·摩尔的经验之谈,其核心内容为:集成电路上可以容纳的晶体管数目在大约每经过18个月便会增加一倍。换言之,处理器的性能每隔两年翻一倍。

每过18个月,硬件性能就能提升一倍,随着计算机的发展,我们的运行设备现在内存越来越大,所以我们不再特别关注空间复杂度。

时间复杂度

算法的时间复杂度是一个函数(数学中的函数),==算法中的基本操作的执行次数为算法的时间复杂度,时间复杂度不是去计算准确时间的,因为我们是没办法准确的计算时间的,比如说冒泡排序,对10w个数字排序,在一台最新酷睿i7和8g内存电脑上可能很快,在一台10年前i3和2g电脑上可能慢很多,算法要计算准确的时间,需要跟运行环境有关。

即找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

例1:

// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
	int count = 0;
    for (int i = 0; i < N ; ++ i)
    {
        for (int j = 0; j < N ; ++ j)
        {
        	++count;
        }
   	}
}
for (int k = 0; k < 2 * N ; ++ k)
{
    ++count;
}
    int M = 10;
    while (M--)
    {
        ++count;
    }

首先写出算法的时间复杂度函数:

F(N)=N*N+2*N+10

N = 10时 F(N) = 130
N = 100时 F(N) = 10210
N = 1000时 F(N) = 1002010

我们发现后两项,N越大,对结果影响越小,实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

大O渐进表示法(估算)

大O符号(Big O notation):是用于描述函数渐进行为的数学符号

大O的渐进表示法:

1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且系数不是1,则去除与这个项的系数。得到的结果就是大O阶。

所以在复杂度中1不是代表1次,代表常数次。

保留对结果影响最大的一项(次数最高的一项),去掉系数。

有些算法的时间复杂度存在最好、平均和最坏情况:

最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)

例如:在一个长度为N数组中搜索一个数据x

最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到

而时间复杂度关注最坏的情况,用最坏去衡量这样的算法,用最坏是靠谱的,这个算法一定不会比这个更差了。

例2:

// 计算Func3的时间复杂度?
void Func3(int N, int M)
{
    int count = 0;
    for (int k = 0; k < M; ++ k)
    {
        ++count;
    }
    for (int k = 0; k < N ; ++ k)
    {
        ++count;
    }
        printf("%d\\n", count);
}

O(M+N)或者O(max(M,N))

当M和N相差不大时,O(M)和O(N)都可以

如果能说明M远大于N,就是O(M)

如果能说明N远大于M,就是O(N)

例3:

// 计算Func2的时间复杂度?
void Func2(int N)
{
	int count = 0;
    for (int k = 0; k < 2 * N ; ++ k)
    {
        ++count;
    }
    int M = 10;
    while (M--)
    {
        ++count;
    }
	printf("%d\\n", count);
}

时间复杂度函数:2*N+10,时间复杂度:O(N).

例4:

// 计算Func4的时间复杂度?
void Func4(int N)
{
	int count = 0;
    for (int k = 0; k < 100; ++ k)
    {
    	++count;
    }
	printf("%d\\n", count);
}

时间复杂度函数为:F(N)=100,根据大O阶法,时间复杂度为:O(1)。

例5:

// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character )
{
    if(*str==character)
    {
        return *str;
    }
    else
    {
        str++;
    }
}

假设字符串长度为N:

最坏情况:N次

最好情况:1次

平均情况:N/2

用最坏情况是衡量这样的算法,用最坏是靠谱的,因为这个算法一定不会比这个更差了。

例6:

// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
    assert(a);
    for (size_t end = n; end > 0; --end)
    {
    	int exchange = 0;
        for (size_t i = 1; i < end; ++i)
        {
            if (a[i-1] > a[i])
            {
                Swap(&a[i-1], &a[i]);
                exchange = 1;
            }
        }
        if (exchange == 0)
            break;
    }
}

很显然这是一个冒泡排序

end等于n时,里面的循环需要进行n次判断,end等于n-1时,里面的循环需要进行n-1次判断,依此类推,可以得到时间复杂度函数。

准确次数复杂度函数:

*F(N)=N+N-1+N-2+…+2+1=(N+1)N/2

F(N)=N^2/2+N/2

时间复杂度:O(N)=N^2

例7:

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
    assert(a);
    int begin = 0;
    int end = n-1;
    while (begin < end)
    {
        int mid = begin + ((end-begin)>>1);
        if (a[mid] < x)
        	begin = mid+1;
        else if (a[mid] > x)
        	end = mid;
        else
        	return mid;
    }
    	return -1;
}

二分查找时间复杂度

最坏情况下的时间复杂度是:一直折半查找,直到折半到只剩一个数字的时候,要么这个数字是查找的数字,要么不是,就查找结束了

所以我们查找一次,N折半一次,所剩范围相当于N/2,查找X次,我们N/2^X,直到结果为1。找了多少次就要折半多少次

即:N/2/2/2/2…/2 = 1

假设找了X次,即:N/2^X=1

则:N=2^X

即:X=log2N

算法里面很多时候,如果不是编辑器支持,不好敲底数,所以一般情况复杂度计算,可以把log2N简化成logN,有不少书上或者网上的资料还喜欢简化为O(lgN),严格来说这个是不对,因为跟数学中就混了,所以注意不要这样写,但是如果别人写了,我们也要知道那其实是log2N

所以二分查找的时间复杂度为O(N)=log2N

例8:

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
    if(1 == N)
    	return 1;
    return Fac(N-1)*N;
}

递归算法时间复杂度如何计算呢?

每次递归的累积次数相加

n个1次相加是N次,所以算法时间复杂度为O(N)。

例9:

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
    if(N < 3)
    	return 1;
    return Fib(N-1) + Fib(N-2);
}

函数递归调用如上图所示,一共有2^N-1个调用,每次递归调用执行的次数为1,故时间复杂度为O(N)=2^N。

一般算法常见的复杂度如下:

我们发现O(1)和O(logn)的时间复杂度相近,这两个相较于其他是最优的,O(2^n)的时间复杂度,可由曲线可见,增长速度很快,所以这种时间复杂度的代码一般是不行的。

空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

注意:

时间是累积的,而空间是不累积的(可以重复利用)

// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
    assert(a);
    for (size_t end = n; end > 0; --end)
    {
    	int exchange = 0;
    for (size_t i = 1; i < end; ++i)
    {
    	if (a[i-1] > a[i])
    	{
   	 		Swap(&a[i-1], &a[i]);
    		exchange = 1;
    	}
    }
    if (exchange == 0)
    	break;
    }
}

创建变量的个数是常数个,所以空间复杂度为:O(1)

额外开辟的空间的空间复杂度:

// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
    if(n==0)
    	return NULL;
    long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
    fibArray[0] = 0;
    fibArray[1] = 1;
    for (int i = 2; i <= n ; ++i)
    {
    	fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
    }
    return fibArray;
}

空间复杂度:O(N)

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
    if(N == 0)
    	return 1;
    return Fac(N-1)*N;
}

递归调用N个函数会开辟N个函数栈帧,每一个函数都是O(1),N个这样的函数就是O(N)

// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
    if(N < 3)
    	return 1;
    return Fib(N-1) + Fib(N-2);
}

时间是累积的,空间是不累积的(可以重复利用),我们在递归调用时,在开始调用时,我们一直调用到fib(2)时函数开始返回,函数只要一返回,空间就释放掉了,而fib N到fib 2一共创建了N-1个函数栈帧,因为函数在返回时,就释放还给操作系统了,所以这段代码最多用了N-1个函数栈帧,一个函数的空间复杂度为O(1),这N-1个就为O(N-1),又根据大O阶法,所以空间复杂度为:O(N)

以上是关于数据结构之时间复杂度与空间复杂度的主要内容,如果未能解决你的问题,请参考以下文章

衡量程序运行效率之时间复杂度和空间复杂度

读《趣学算法》:重开算法之门,时间复杂度与空间复杂度

数据结构之 - 时间复杂度/空间复杂度

『数据结构与算法』之时间复杂度与空间复杂度,看这一篇就够啦

数据结构之时间复杂度与空间复杂度

时间复杂度与空间复杂度