一、数据结构的介绍
1.什么是数据结构
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。通俗来讲就是用来处理数据的一个工具,它还有个兄弟叫数据库,不过二者有着一些小小的区别。数据结构在内存里管理数据,存储快,带点存储;数据库在硬盘里管理数据,存储慢,不带电存储。
2.什么是算法
算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
二、算法复杂度的介绍
1.算法效率
衡量一个算法的效率从两个仿方面来看
1.算法的时间复杂度
2.算法的空间复杂度
2.时间复杂度
2.1什么是时间复杂度
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知
道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
按照通俗来讲就是这个程序块执行了几次。下面通过一个案例来讲解
void test1(int 2n,int m)
{
int a=0;
int b=0;
int c=0;
for(int i=0;i<2n;i++)
{
a=i;
}
for(int k=0;k<m;k++)
{
b=k;
}
for(int j=0;j<10;j++)
{
c=j;
}
}
从上面的例子我们可以看到,三个循环分别执行了2N、N、10次。它的执行次数表达式F(N)=2N+N+10;但是它的时间复杂度为O(N)。
2.2大O的渐进表示法
首先,我们要明确一个概念,大O的渐进表示法只是大概表述程序块所执行的次数,并不是精确的,因此我们存在使用以下四种方法来表述。
1、用常数1取代运行时间中的所有加法常数。
void test2()
{
int a,b,c;
for(int i=0;i<10;i++)
{
a=i;
}
for(int k=0;k<20;k++)
{
b=k;
}
for(int j=0;j<30;j++)
{
c=j;
}
}
上面例子中的代码块一共执行了10+20+30=60次。但根据大O表示法我们将常数表示为1。所以它的时间复杂度为O(1)。
2、在修改后的运行次数函数中,只保留最高阶项。
void test3(int n)
{
int a,b;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
a=j;
}
}
for(int k=0;k<n;k++)
{
int a=k;
}
}
上面例子中的代码块执行了N2+N次。但根据大O表示法,只保留最高阶项,其余全部清除。因此它的算法复杂度为O(N2)。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
void test3(int n)
{
int a,b;
for(int i=0;i<n;i++)
{
for(int j=0;j<2n;j++)
{
a=j;
}
}
for(int k=0;k<n;k++)
{
int a=k;
}
}
上面例子中的代码执行次数为2*(N2)+N次。但是根据大O表示法最高阶项前面的次数不是1,应将其改为1。因此它的算法复杂度为O(N^2)。
从上述我们可以看出,大O(N)表示法的核心都是将影响程序执行的最大的次数保留了下来,其余统统删去,常数用1来表达。这类似于我们数学中的极限思想。
上面所列举的例子执行次数是一定的,但现实中还有一些代码块的执行次数不能确定,那我们应如何确定他们的时间的复杂度呢?
4、有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
上述例子我们看到查找str的执行次数为1—N次,程序执行次数不确定。这时时间复杂度应为最坏的执行情况。因此,它的时间复杂度为O(N)。
5.上述例子我们都是根据程序的表面判断出程序的复杂度计算。但是现实中有许多算法是我们需要根据它的具体执行思想来判断出时间复杂度的。
例一:
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
这个例子中函数一共调用了N次,每次调用时执行程序块if为一次。因此一共执行了N*1次。时间复杂度为O(N)次。
例二:
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
为了方便分析,我们画出程序执行的二叉图。可以看到当我们看到程序调用了N-1次。每次调用时都会“分化”两个函数,然后继续“分化”,直到N<3停止。成为了一个等比数列。它的程序执行次数的中和为F(n)=2(n-1)-20。因此它的时间复杂度为O(2n)。
例三:
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号 while (begin <= end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid-1;
else
return mid;
}
return -1;
}
这个例子是二分查找。同样的,为了方便理解,我们画出草图。我们设执行的次数为X次,每次需要从中间开始查找,然后进行判断大小并选定区间的中间值,如此循环,知道找到我们想要的数字。当然,这里有最坏~最好的情况。根据大O表示法,我们算最坏,即最后二分到最后一个数字才找到我们想要的数字。根据图上的2x=N得X=logN。故时间复杂度为O(logN)。
3.空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时额外占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
例一:
`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;
}
}
上述冒泡排序例子中变量 exchange、i、end是重新定义的变量,数目为常数个。根据大O表示法,这个程序的空间复杂度为O(1)。
例二:
void test(int arr[],int n)
{
int*p=(int*)malloc(sizeof(arr)*n);
int i=0;
for( i=0;i<n;i++)
{
p[i]=arr[i];
}
}
上述例子中我们重新定义了一个变量i和重新开辟了一个大小为n的可以存放整形变量的数组p。
此时一共重新开辟了N+1个变量,并且在函数调用过程中使用了所开辟的空间。根据大O表示法,空间复杂度为O(N)。
例三,有关递归调用计算空间复杂度的例子
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
这个例子是一个递归调用。对于递归调用的空间复杂度计算来说,它也是调用空间的累加(和递归时间复杂度不同的是,递归调用时,空间可以反复利用),我们在递归的过程中开辟了N个空间。它的空间复杂度为O(N)。
例四
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
递归函数在调用的时候一直开辟空间,在回归的时候空间会收回。但在收回以后要是还有调用的情况,那使用的空间会是调用前收回的那块空间(即空间的反复使用)。对于这个例子来说,就是这样的。因此空间复杂度是O(N)。