什么是数据结构:
数据结构是计算机存储,组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
什么是算法:
算法就是定义良好的计算过程,他去一个或一组的值为输入,并产生一个或一组值作为输出,简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
学好数据结构和算法重点是练习(牛客网和leetcode)
算法时间复杂度和空间复杂度:
算法在编写可执行程序后,运行需要耗费时间资源和空间(内存)资源,因此衡量一个算法的好坏,一般是从时间复杂度和空间两个维度来衡量的。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。(现在不再关注空间)
时间复杂度的定义:
在计算机中,算法的时间复杂度是一个函数(数学里面带有未知数的函数表达式),他定量的描述了该算法的运行时间,一个算法执行所消耗的时间,从理论上是不能算出来的,只有你把程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机调试吗?是可以,但是很麻烦,所以引入时间复杂度的分析方法。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
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);
}
时间复杂度的函数式: Func1=N*N+2*N+10
N越大,后两项对结果的影响越小,即实际中我们计算时间复杂度时,我们并不是计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O的渐进表示法:用于描述函数渐进行为的数字符号
推导大O阶方法:
- 用常数1取代运行时间中所有的加法常数;注:O(1)不是代表运算一次,而是常数次
- 在修改后的运行函数中,只保留最高阶项;
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶
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);
}
Func2=O(N)
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);
}
Func2=O(M+N)
一般情况下时间复杂度计算时未知数都是用的N,但是也可以用M,K等其他。
- M远大于N->O(M)
- N远大于M->O(N)
- M和N差不多->O(M)或O(N)
const char * strchr(const char * str,int character)
//等价于
while(*str)
{
if(*str==character)
return str;
else ++str;
}
大O的渐进法表示去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数,另外有些算法时间复杂度存在最好,平均和最坏情况:
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N的数组中搜索一个数据x;最好情况:1次找到;最坏情况:N次找到;平均情况:N/2次找到。
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
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;
}
}
冒泡排序:F(N)=N*(N-2)/2 时间复杂度:O(N^2)
每一趟只能确定将一个数归位。即第一趟只能确定将末位上的数归位,第二趟只能将倒数第 2 位上的数归位,依次类推下去。如果有 n 个数进行排序,只需将 n-1 个数归位,也就是要进行 n-1 趟操作。而 “每一趟 ” 都需要从第一位开始进行相邻的两个数的比较,将较大的数放后面,比较完毕之后向后挪一位继续比较下面两个相邻的两个数大小关系,重复此步骤,直到最后一个还没归位的数。
void 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;
}
}
二分查找算法:时间复杂度是: log 2 ( N ) ,每次除以二(折中)
先找到那个有序序列的中间元素mid,然后拿它和要找的元素K进行比较,就可以初步判断K所在范围,既然查找范围已确定,自然该范围之外的元素就可以不用再查找了(你看这样相较于顺序查找一下子就可以省略一半的元素不用查找了,这就是效率啊!!!)。当然接下来还会按照上面的步骤反复查找下去。
二分查找法的前提条件:
查找的序列必须是有序的。我想大概很多小伙伴都会错误的认为有序是差值恒为1的顺序数列(就像这样1、2、3、4、5、6、7、8、9)。这只不过是有序的某一种情况罢了,那何为有序呢?即该序列中的所有元素都是按照升序或者降序排列好的,元素与元素只间的差值虽然是随机的,但始终是在递增或递减(例如这样一个序列:3、12、24、31、46、48、66、79、82)。
二分查找有两个限制条件:
- 查找的数量只能是一个,不能是多个
- 查找的对象在逻辑上必须是有序的
long long Fac(size_t N)
{
if(0==N)
return 1;
return Fac(N-1)*N;
}
阶乘:时间复杂度是: O ( N )
long long Fib(size_t N)
{
if(N<3)
{
return 1;
}
return Fib(N-1)+Fib(N-2);
}
斐波那契数列:时间复杂度是: O ( 2^N ) (2^0+2^1+2^2+...+2^(n-1))
递归:
递归算法是一种直接或者间接调用自身函数或者方法的算法。说简单了就是程序自身的调用。递归算法就是将原问题不断分解为规模缩小的子问题,然后递归调用方法来表示 问题的解。(用同一个方法去解决规模不同的问题)
- 递去:将递归问题分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决
- 归来:当你将问题不断缩小规模递去的时候,必须有一个明确的结束递去的临界点(递归出口),一旦达到这个临界点即就从该点原路返回到原点,最终问题得到解决。
递归算法的设计要素:
- 明确递归的终止条件
- 提取重复的逻辑,缩小问题的规模不断递去
- 给出递归终止时的处理办法
空间复杂度:
- 空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度;
- 空间复杂度不是程序占用了多少比特的空间,因为无意义,所以空间复杂度是变量的个数
- 空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进法
注意:函数运行时所需要的栈空间(存储参数,局部变量,一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要是通过函数在运行时候显示申请的额外空间来确定的。
void BubbleSort(int *a,int n)
{
assert(a);
for(size_t i=1;i<end;++i)
{
int exchange=0
if(a[i-1]>a[i])
{
swap(&a[i-1],&a[i]);
exchange=1
}
if(exchange==0)
break;
}
}
冒泡排序:
空间复杂度:end和i两个 O(1)
long long Fibonacci(size_t n)
{
if(n==0)
{
return null;
}
long long *fibArray=(long long *)malloc((n+1)*sizeof(long long));//n个数的数组
fibArray[0]=0;
fibArray[1]=1;
for(int i=2;i<=n;++i)
{
fibArray[i]=fibArray[i-1]+fibArray[i-2];
}
return fibArray;
}
斐波那契序列:
空间复杂度: n个数组 O(N)
long long Fac(size_t N)
{
if(0==N)
return 1;
return Fac(N-1)*N;
}
递归:阶乘
空间复杂度:递归的深度 O(N)
long long Fib(size_t N)
{
if(N<3)
{
return 1;
}
return Fib(N-1)+Fib(N-2);
}
斐波那契数列:
空间复杂度:空间是可以重复利用不累计,时间是不能重复利用是累计 O(N)