时间复杂度

目录

1. 时间复杂度的概念

2. 大O渐近表示法

3. 例题

3.1 消失的数字

思路解析:

示例代码:

3.2 轮转数组

思路解析:

示例代码:

3.3 计算阶乘递归Fac的时间复杂度

思路解析:

3.4 计算斐波那契递归Fib的时间复杂度

思路解析:


1. 时间复杂度的概念

计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。

算法中的基本操作的执行次数,为算法的时间复杂度。

为什么要用时间复杂度来描述算法的运行时间?

举个例子,同样的一段代码,放在学校机房老掉牙的电脑上跑 和 在用i9的CPU的游戏本上跑,所花费的时间显然是不一样的。代码运行时间还会被电脑的负载、硬件资源等等方面影响。

所以,直白的用运行时间进行评估是不可以的。

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

示例:计算Func1的时间复杂度。

void Func1(int N)
{
	int count = 0;
	for (int i = 0; i < N; ++i)
	{
		for (int j = 0; j < N; ++k)
		{
			++count;
		}
	}

	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}

	int M = 10;
	while (M--)
	{
		++count;
	}
}

一条语句的重复执行次数称为“语句频度”。 

两个嵌套的for循环语句频度是N^2,第二个一层的for循环是2N,while循环是10(因为M = 10)。

所以可以得到时间复杂度(一个带未知数N的函数式): F(N)=N^2+2N+10

2. 大O渐近表示法

对于Func1()这种比较简单的算法,可以直接计算出所有语句的频度;但是对于稍微复杂一点的算法,计算所有语句的频度式比较困难的。

所以,为了客观反映一个算法的执行时间,可以只用算法中的基本语句的执行次数来衡量算法的工作量。所谓的“基本语句”就是对算法运行时间影响最大的语句

这就是 大O渐近表示法,这种方法属于估算它属于那个量级。

举个例子,算财富值可以分为 超级富豪、富豪、中产等等,身价多少属于哪个等级,不会计较在富豪里是一亿还是两亿。

大O渐近表示法的推导:

1. 用常数1取代运行时间中的所有加法常数;

2. 在修改后的运行次数函数中,只保留最高阶项

3. 如果最高阶项存在且不为1,则去掉与这个项相乘的常数,得到的结果就是大O阶

所以使用大O渐近表示法后,Func1的时间复杂度为 O(N^2)

常见的时间复杂度有 :O(1)O(N)O(N^2)O(N^3)O(N*logN)O(logN)

示例:计算Func2的时间复杂度。

void Func2(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);
}

因为M和N的具体的值没有给出,Func2的时间复杂度是 O(M+N)

假如条件中有:

M远大于N,可以写成 O(M)

N远大于M,可以写成 O(N)

示例:计算Func3的时间复杂度。

void Func3(int N)
{
	int count = 0;
	for (int k = 0; k < 100; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}

Func3的时间复杂度是 O(1)

注意:O(1)并不是代表1次,代表常数次。

示例:计算strchr的时间复杂度。

const char* strchr(const char* str, int character);

这是一个字符查找函数,大致的过程是:

while (*str)
{
	if (*str == chraracter)
	{
		return str;
	}
	else
	{
		++str;
	}
}

 这个函数的时间复杂度和前面的不太一样,因为字符串长度是未知的。

运气好的话,第一次就能找到目标;

运气一般般,在中间找到目标;

运气贼差,那就在最后一次找到。

所以,有一些算法的时间复杂度是存在最好、平均、最坏的情况

最好情况:任意输入规模的最小运行次数(下限);

平均情况:任意输入规模的期望运行次数;

最坏情况:任意输入规模的最大运行次数(上界)。

在实际情况关注的是算法的最坏运行时间,所以数组中搜索数据时间复杂度为O(N)

示例:计算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;
	}
}

BubbleSort的时间复杂度是 O(N^2)

冒泡排序的语句频度是等差数列\frac{N*(N-1)}{2}

示例:计算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;
		else if (a[mid] > x)
			end = mid - 1;
		else
			return mid;
	}
	return -1;
}

二分查找的本质是缩小区间,区间最开始有N个值,不断减半

最差的情况是:直到N/2/2/2.../2 == 1,区间里只有一个数,这个数就是要找的。

除了多少个2,就查找了多少次,根据这个性质可以推出:N=2^x

那么,x=\log_{2}N

所以,BinarySearch的时间复杂度是O(\log N)

注意:通常我们会写成O(\log N),这是因为对数不好书写,除非是用专业公式支持的编辑器(只是在时间复杂度这儿)

3. 例题

3.1 消失的数字

思路解析:

思路一:先排序,在后一个数不等于前一个数+1,那么本该的后一个数就是消失的数字。

如果使用冒泡排序,冒泡的时间复杂度是O(N^2),遍历是O(N)

如果使用qsort,时间复杂度是O(N*\log N)

思路二:创建一个x先和0~n的所有值异或,再和数组中的值异或,结果就是消失的数字。

(N + 1) + N,所以时间复杂度是O(N)

思路三:计算0~n累加的和,再用和 减数组中的值,结果就是消失的数字。

时间复杂度是O(N)

示例代码:

//思路二
int missingNumber(int* nums, int numsSize){
    int x = 0;
    int n = numsSize;
    for(int i = 0; i <= n; ++i)
    {
        x ^= i;
    }
    for(int j = 0; j < numsSize; ++j)
    {
        x ^= nums[j];
    }
    return x;
}
//思路三
int missingNumber(int* nums, int numsSize){
    int sum = 0;
    for(int i = 0;i<numsSize+1;i++)
    {
        sum+=i;
    }
    for(int i = 0;i<numsSize;i++)
    {
        sum-=nums[i];
    }
    return sum;
}

3.2 轮转数组

思路解析:

思路一:每次挪动旋转一位,右旋k次。最好情况:k是N的倍数,此时相当于没有旋转。时间复杂度是O(1)。最坏情况:k % N == N - 1,此时的时间复杂度是O(N^2)

思路二:三步旋转法。前n-k个逆置,然后后k个逆置,最后整体逆置。(N - k) + k + N = 2N。时间复杂度是O(N)

思路三:新建一个数组,把轮转的值放进去,返回新数组。时间复杂度是O(N)

示例代码:

//思路二
void Reverse(int* arr, int left, int right){
    while(left < right)
    {
        int tmp = arr[left];
        arr[left] = arr[right];
        arr[right] = tmp;
        ++left;
        --right;
    }
}

void rotate(int* nums, int numsSize, int k) {
    k %= numsSize;
    //逆置前n-k个
    Reverse(nums, 0, numsSize-k-1);
    //逆置后k个
    Reverse(nums, numsSize-k, numsSize-1);
    //整体逆置
    Reverse(nums, 0, numsSize-1);
}
//思路三
void rotate(int* nums, int numsSize, int k) {
    int tmp[numsSize];
    k %= numsSize;
    int n = numsSize;

    memcpy(tmp, nums + n - k, sizeof(int)*k);
    memcpy(tmp + k, nums, sizeof(int)*(n - k));
    memcpy(nums, tmp, sizeof(int)*n);

}

3.3 计算阶乘递归Fac的时间复杂度

long long Fac(size_t N) {
	if (0 == N)
		return 1;
	return Fac(N - 1) * N;
}

思路解析:

递归算法的时间复杂度是每次递归子函数消耗累加起来。

所以,时间复杂度是O(N)

3.4 计算斐波那契递归Fib的时间复杂度

long long Fib(size_t N) {
	if (N < 3)
		return 1;
	return Fib(N - 1) + Fib(N - 2);
}

思路解析:

所以,Fib的时间复杂度是O(2^N)

  • 31
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

YMLT花岗岩

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

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

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

打赏作者

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

抵扣说明:

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

余额充值