欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客
https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (BingbingSuperEffort) - Gitee.com
https://gitee.com/BingbingSuperEffort
目录
一、数据结构
数据结构定义:
数据结构是计算机存储、组织数据的方式,指相互之间一种或多种特定关系的数据元素集合。
数据结构和数据库的区别:
数据结构和数据库本质上都是存储管理数据的,但是数据结构是在内存中存储管理数据,数据库是在磁盘中存储管理数据。
二、算法复杂度
算法定义:
算法就是定义良好的计算过程,他取一个或一组的值为输入,并产生一个或一组值作为输出,简单来说,算法就是一系列的计算步骤,用来输入数据转化成输出结果。
算法的复杂度:
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
2.1时间复杂度
时间复杂度的定义:
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知 道。
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法 的时间复杂度。
但是,我们并非每一条语句都去计算,而是知道大概执行次数就可以计算复杂度,那么我们就得使用大O的渐进表示法。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
通俗来说就是找到最大的量级来代替精准的执行次数。
我们计算的复杂度为算法的最坏的情况。
我们不能简单的通过数循环的方式来确定复杂度,应该理解其中的思想。
例如下面的练习:
例1:冒泡排序
冒泡排序中含有两个循环,我们先计算以下具体的时间复杂度,end的取值为n,每次减少1,所以外侧循环将进行n次,内测循环是否也是n次呢?在最坏的情况下,内测循环进行n-1次,每次都会递减,因此两个循环的具体复杂度应该为一个等差数列。
所以:
用大O表示法就是只保留最高阶项,并取出系数,所以得到的结果为O(N^2)
例2:二分查找
在二分查找中,end的取值为n-1,begin为0;有一个循环,循环条件是begin<end。那么时间复杂度就是O(N)吗?
并不是,我们还是要看代码的思路,每进行一次二分查找,我们是将区间进行折半,而不是逐渐递减。在最坏的情况下,我们最终将区间缩短的只剩1个数找到,或者最终也没找到,也就是说我们进行查找的次数就是乘2的次数。2^x=N
那么二分查找的大O表示为
注意:由于以2为底的log经常出现,我们常常在写大O表示法的时候会省略底数2,直接写成logN,有些参考书中写的lgN我们也要将其认为是以2为底的log
例3:斐波那契数列递归算法
递归法求斐波那契数时的时间复杂度多少呢?我们发现,每次递归调用函数,都会产生两次新的递归函数调用,如下图所示。
所以递归法求斐波那契数的时间复杂度为O(2^N) 这也就解释了为什么此种方法的效率低的原因。
2.2空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间而是计算额外开辟的变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时显式申请的额外空间来确定。
现在我们来看一下例1中冒泡排序的空间复杂度
在冒泡排序中,传入过来的数组并不是我们额外开辟的空间,我们直接使用即可,我们创建的变量为size_t end, size_t i, int exchange以及交换函数的空间和交换函数中的临时变量tmp。
这些变量是可数的,因此空间复杂度为O(1).
现在我们在分析一下斐波那契数递归算法的空间复杂度。
我们知道该算法的时间复杂度为O(2^N),因为我们调用了2^N个函数,所以我们为2^N个函数开辟了空间,因此空间复杂度为2^N。
是这样吗?显然不是,如果是这样的话,在用该算法计算第10个斐波那契数的时候就栈溢出了,因为我们开辟了100W个空间,但是计算机并没有栈溢出,说明空间复杂度并不是O(2^N).
我们知道,函数在调用时会在栈上创建空间,但是函数调用完成后,空间就会释放,并不会一直存在。
也就是说在递归调用时,先一条路走到头,然后返回一步销毁一步,在调用新的函数,新函数使用的是旧函数销毁的空间,并不会重新开辟空间。所以最多也就是开辟N次调用的空间,空间复杂度为O(N) .
因此我们要注意:时间是累积的,空间并不累积。
三、复杂度的两道练习题
3.1消失的数字
题目链接:面试题 17.04. 消失的数字 - 力扣(LeetCode) (leetcode-cn.com)
题目:数组nums
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
题目讲解:
该题设置了时间复杂度的要求,让我们在O(N)时间内完成。
解法一:对数组进行排序,然后返回数组下标。
什么意思呢?
既然数组中的元素为0~n,只是缺少了一个,那我们对数组从小到大进行排序,得到的数组元素在缺失数字之前必然是下标对应的就是数字本身,如果从头开始遍历数组,发现第一个下标不符合对应的数字,说明该下标就是缺失的数字。
例如,数组nums包含0~7之间的数字,唯独缺少6。分析如下:
下标0~5对应的都是数字0~5,但是下标6对应的数字却是7,说明缺少数字6.
代码如下:
int missingNumber(int* nums, int numsSize)//排序之后遍历
{
int i = 0;
for (i = 0; i < numsSize; i++)
{
int flag = 0;
int j = 0;
for (j = 0; j < numsSize - i - 1; j++)
{
if (nums[j] > nums[j+1])
{
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = 1;
}
}
if(flag==0)
{
break;
}
}
for (i = 0; i <numsSize; i++)
{
if (nums[i] != i)
return i;
}
return numsSize;
}
但是该方法时间复杂度符合吗?如果使用冒泡排序进行排序,算法复杂度就已经超过了O(N),而是O(N^2)。所以不建议此种方法。
解法二:以空间换时间,创建额外数组。
第二种方法是创建一个包含0~n个元素的数组,然后将其初始化为-1,对原来的数组nums进行遍历,将数组中的元素对应的放入下标为元素大小的新数组中,然后遍历新数组,那个下标对应的元素为-1,哪个下标就是缺少的元素。
分析:
代码:
#include<stdlib.h>
int missingNumber(int* nums, int numsSize)//创建额外数组
{
int* p = (int*)malloc((numsSize + 1) * sizeof(int));
int i = 0;
for (i = 0; i < numsSize + 1; i++)
{
p[i] = -1;
}
for (i = 0; i < numsSize; i++)
{
p[nums[i]] = nums[i];
}
for (i = 0; i < numsSize + 1; i++)
{
if (p[i] == -1)
{
return i;
}
}
}
该算法的时间复杂度为O(N)满足要求,空间复杂度为O(N),但是一般不会限制空间复杂度,所以代码可以跑过去。
解法三:异或
该算法采用了异或的相关知识,我们都知道,0与任何数异或都得数本身,两个相同的数异或得0.
由于我们不知道0~n中缺少的数字,但是我们将数组中每个数字异或在一起,然后在与0~n的数字异或在一起,这样缺少的数字只会出现一次,其余数字都出现两次,然后异或为0,最终异或的结果就是缺少的数字本身。
该算法时间复杂度为O(N),空间复杂度为O(1)。
代码:
int missingNumber(int* nums, int numsSize)//异或
{
int x = 0;
int i = 0;
for (i = 0; i <= numsSize; i++)
{
x ^= i;
if (i != numsSize)
{
x ^= nums[i];
}
}
return x;
}
解法四:加减法
加减法的算法是最好理解的,就是将0~n个数字全部加起来,然后减掉nums数组中的元素,缺少的数字就会算出来了。
该算法时间复杂度为O(N),空间复杂度为O(1)。
代码:
int missingNumber(int* nums, int numsSize)//减法
{
int x = 0;
int i = 0;
for (i = 0; i <= numsSize; i++)
{
x += i;
if (i != numsSize )
{
x -= nums[i];
}
}
return x;
}
3.2旋转数组
题目链接:189. 轮转数组 - 力扣(LeetCode) (leetcode-cn.com)
题目:给你一个数组,将数组中的元素向右轮转 k
个位置,其中 k
是非负数
该题目的限制是时间复杂度为O(N),空间复杂度最好为O(1).
解法一:使用循环逐步轮转
这种算法是我们最先想到的,毕竟题目的给我们讲解轮转过程用的也是这种方法
具体做法就是进行n次循环,每次循环轮转一个数字 。我们就需要创建一个临时变量来存储每次轮转过程中的最后一个数字,然后将前n-1个数字整体向后移动一下,在把tmp中的数字放到数组开头。
由于k的取值可以大于n,但是轮转10次和轮转3次是一样的,所以k需要模等n。
此种方法下,空间复杂度为O(1),但是时间复杂度为O(N*K),轮转次数K最大可为N-1,因此时间复杂度并不符合O(N)而是O(N^2)。
代码:整体移动使用的库函数memmove
void rotate(int* nums, int numsSize, int k)
{
k %= numsSize;
while ( k )
{
int tmp = nums[numsSize - 1];
memmove(nums + 1, nums, sizeof(int) * (numsSize - 1));
nums[0] = tmp;
k--;
}
}
解法二:空间换时间,建立额外数组进行拷贝
该算法的核心是使用malloc额外开辟一个空间大小为n个元素的数组,将原数组的后k个元素拷贝放到新数组的前面,再将原数组前n-k个元素拷贝放到新数组的后面,最后将新数组的内容整体拷贝回原数组。时间复杂度为O(N),空间复杂度为O(N)。
代码:
void rotate(int* nums, int numsSize, int k)
{
k %= numsSize;
int* p = (int*)malloc(sizeof(int) * numsSize);
int i = 0;
int j = 0;
for ( i = numsSize - k; i < numsSize; i++ )
{
p[j++] = nums[i];
}
for ( i = 0; i < numsSize - k; i++ )
{
p[j++] = nums[i];
}
for ( i = 0; i < numsSize; i++ )
{
nums[i] = p[i];
}
free(p);
p = NULL;
}
解法三:翻转三次
该解法一般人是真的想不到,不得不佩服想出此解法的大神。
首先我们对后k个数进行反转,然后对前n-k个数进行反转,然后整体进行反转。
代码:
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)
{
k %= numsSize;
reverse(nums, numsSize - k,numsSize-1);
reverse(nums, 0,numsSize-k-1);
reverse(nums, 0,numsSize-1);
}