数据结构<1>时间复杂度详解和leetcode例题

什么是时间复杂度和空间复杂度

前言(算法效率)

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。
时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间

时间复杂度的计算

时间复杂度是一个衡量算法时间的相对标准。他不是一个固定的时间例如多少分多少秒,而是一个大概的估计数值。比如同一个算法在不同的机器上运行的时间肯定不同。这时候就需要我们的时间复杂度来衡量算法的时间效率。

时间复杂度通常使用大O的渐进表示法(空间复杂度也是)

那么什么是大O的渐进表示法呢?

列举几个实例给搭建看看 O(N),O(N^2),O(1);

如上所示就是大O的渐进表示法。括号内就是算法执行的次数,要记住时间复杂度计算的是执行次数
但是他不是一个准确的数字而是一个估计值,比如(N^2+2N+10)在N很大的时候除了最高次项其他的都可以忽略不计,这类似与数学中的极限思想,只保留对结果影响最大的哪一项。

推导法则如下:
1.若括号内是常数都写成O(1)
2.若是括号内是多项式例如O(N^2+3n+9)则只保留最高次项
3.若是最高次项的系数不为1则统一写成1

下面我们来看几个例子学习一下:

// 请计算一下Func1基本操作执行了多少次?
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;
}
printf("%d\n", count);
}

这个算法的具体执行次数是(N*N+2N+10)
而他的时间复杂度是O(N^2)

// 计算strchr的时间复杂度?
const char * strchr ( const char * str, char character )
{
while(*str != '\0')
{
if(*str == character)
return str;
++str;
}
return NULL;
}

要注意有的算法有最坏情况,最好情况和平均情况。比如上面这个算法是在一个字符串中寻找一个字符,最好情况就是一上来就找到了,时间复杂度是O(1),最坏情况是遍历完了字符串才找到或者没找到时间复杂度为O(N),平均情况为O(N/2);

我们计算时间复杂度一般是以最坏情况

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n;
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个元素我们查找了x次2^x = N所以**x = log2(N);

但是在算法中我们一般写成O(logN);

// 计算阶乘递归Factorial的时间复杂度?
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}

这是递归的时间复杂度计算。

每次递归的时间复杂度都是O(1),递归进行了N次,所以这个算法的时间复杂度是O(N);

插入一点:递归的效率在时间复杂度相同的情况下比循环低,因为递归需要创建函数栈帧,而循环不需要

//循环求斐波那契数列

#include<stdio.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	
	int* fib = (int*)malloc(n * (sizeof(int)));

	fib[0] = 1;
	fib[1] = 1;
	for (int i = 2; i < n; i++)
	{
		fib[i] = fib[i - 1] + fib[i - 2];
	}
	for (int j = 0; j < n; j++)
	{
		printf("%d ", fib[j]);
	}
	return 0;
}

该算法的时间复杂度是O(n);

int show(int *num,int m,int n)
{
    int x = 0;
    for(int i=0;i<m;i++)
    {
        x^=num[i];
    }
    for(int j = 0;j<n;j++)
    {
        x^=num[j];
    }
    return x;
}

上面的算法时间复杂度是O(m+n)要注意我们不明确m和n的关系所以不能乱写,比如若m>>n那时间复杂度就是O(m)反之就是O(n),如果m和n差不多那就是O(n^2) 或 O(m^2)

为了了解各个时间复杂度的差异我们来看一下图
在这里插入图片描述

图上我们可以看到logn与1非常接近,所以这两个都是近乎最好的算法复杂度。

空间复杂度的计算

上文提到,时间复杂度实际上就是计算代码执行次数
而空间复杂度实际上就是计算变量个数

这里需要我们先记住一句话时间是累积的空间是不累积的。后面会解释的
空间复杂度也是用大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;
}
}

形参也会计算在内,所以这段代码的变量共有5个根据大O的渐进表示法为O(1),这里要注意循环了n次并不是o(n)因为上面说到空间是不累积的,每次循环完之后变量都会销毁下一次循环在创建,可以理解为使用同一片空间。

// 计算阶乘递归Factorial的空间复杂度?
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}

递归的空间复杂度是O(n),因为每次递归都会创建一次栈帧,递归在没有递归到底的时候是不会销毁前面的栈帧的(这也就是递归太深会导致栈溢出的原因)。

//循环求斐波那契数列

#include<stdio.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	
	int* fib = (int*)malloc(n * (sizeof(int)));

	fib[0] = 1;
	fib[1] = 1;
	for (int i = 2; i < n; i++)
	{
		fib[i] = fib[i - 1] + fib[i - 2];
	}
	for (int j = 0; j < n; j++)
	{
		printf("%d ", fib[j]);
	}
    free(fib);
	return 0;
}

该代码的时间复杂度是O(n)因为malloc开辟的空间也要计算进去。虽然 最后释放了,但是这就类似于时间复杂度的最坏情况了,我们计算的是最多的时候创建了几个变量,占用多少内存。

oj练习

消失的数字

这里的思路是,将给定数组的所有元素异或起来,然后再与标准的数组进行异或;因为相同数字异或(按位比,相同为0,不同为1)后为0,所以最后剩下的那个数字就是数组消失的数组。
注:相同数字异或为0
任何数字异或上0之后都不变
这里也可以用求和,给定数组求和,减去标准数组之和即是消失的数字。(有溢出的可能)

int missingNumber(int* nums, int numsSize){
    int n = 0;
    for(int i = 0;i<numsSize;++i)
    {
        n^=nums[i];
    }
    for(int j = 0;j<numsSize+1;++j)
    {
        n^=j;
    }
    return n;
}

旋转数组

相信大家一定能想到第一种思路就是

1.把最后一个数组元素保存起来,然后将数组元素右移一个单位,再把最后一个元素放到数组开头位置。但是这样是双层循环,时间复杂度为O(N)所以效率很低。
2.用空间换时间的思想,再创建一个数组,保存nums的后k个元素,然后再保存数组的前numSize-k个元素。这样是需要递归一次数组就可以了。时间复杂度是O(N);
3.最好的方法是,先逆置后k个数组元素,再逆置前numSize-k个数组元素,最后再整体逆置(大神想出来的方法)时间复杂度为O(1)!!!
下面我们来实现一下第三种方法。


void reverse(int *nums,int left,int right)
{
    while(left<right)
    {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
        left++;
        right--;
    }
}

void rotate(int* nums, int numsSize, int k){
    if(k>=numsSize)
    {
        k%=numsSize;
    }
    reverse(nums,numsSize-k,numsSize-1);
    reverse(nums,0,numsSize-k-1);
    reverse(nums,0,numsSize-1);
}
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

KissKernel

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

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

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

打赏作者

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

抵扣说明:

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

余额充值