为了编写出一个“好”程序,必须分析待处理的对象的特征以及各处理对象之间存在的关系。一般来说,用计算机解决一个具体问题时,大致需要以下几个步骤:首先要从具体的问题抽象出一个适当的数学模型,其次设计一个解决该数学模型的算法,编写出程序,并进行测试、调整直至得到最终解答。寻求数学模型的实质是分析问题,从中提取操作的对象,并找出这些操作对象之间的关系,然后用数学加以描述。 因此,我们可以说程序设计=数据结构+算法。下面我们来了解一下数据结构的基本概念。
基本概念
- 数据(data):对客观事物的符号的表示,在计算机科学中是指所有能输入到计算机,并被计算机程序识别处理的符号的总称。
- 数据元素(data element):数据的基本单位,在计算机程序中通常作为整体进行考虑和处理。
- 数据对象(data oject):是性质相同的数据元素的集合,是数据的一个子集。
- 数据项(data item):一个数据元素可以由若干个数据项组成。数据项是数据的不可分割的最小单位。
数据结构
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
物理结构
顺序结构:数据元素存放在地址连续的单元中,数据的逻辑关系和物理关系是一致的。
链式结构:数据元素存放在任意的存储单元中,存储单元可以连续也可以不连续。
时间复杂度
衡量一个算法是不是好算法,要看它的效率。在实际中通常关注的是算法的最坏运行情况,即:任意输入规模N,算法的最长运行时间。因为:
- 一个算法的最坏情况的运行时间是在任意输入下的运行时间的上限。
- 对于某些算法,最坏的运行情况出现的比较频繁。
- 大体上看,平均情况与最坏情况的运行时间基本一样差。
因此:一般情况下使用O渐近表示法来计算算法的时间复杂度。
O渐近表示法:算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作T(n)=O(f(n)),它表示随问题规模n的增大,算法的执行时间的增长率和f(n)的增长率相同,称O(f(n))为算法的渐近时间复杂度,即时间复杂度的O渐近表示法。
时间复杂度简单来说就是语句执行的次数。O渐进表示法是控制变化因素,估算n趋于无穷大的情况,是一个估算值。
O(n)的一般计算方法:
1. 数执行语句的次数,遇到循环用乘法,顺序执行用加法;
2. 用O括起来,即O();
3. 用常数1取代运行时间中的所有加法常数;
4. 扔掉低阶,保留最高阶;
5. 将留下的最高阶的系数换为1。
空间复杂度
空间复杂度:函数中创建对象的个数与问题规模n的函数表达式,一般采用O渐近表示法。其计算步骤与时间复杂度的计算相同,计算的是创建变量的个数。
我们先来看下面这段程序的时间复杂度和空间复杂度。
void test(int n)
{
int count = 0;
int i = 0;
for(i=0;i<10;i++)
{
count++;
}
}
时间复杂度:该程序中有一个for循环,执行的次数为10次,其执行的次数与n无关,根据时间复杂度的计算方法可得该程序的时间复杂度为O(1)。
空间复杂度:该程序从头到尾创建了两个变量,且与n的大小无关,故该程序的空间复杂度为O(1)。
void test(int n)
{
int count = 0;
int i = 0;
for(i=0;i<10;i++)
{
count++;
}
for(i=0;i<n;i++)
{
count++;
}
}
时间复杂度:这段程序有两个for循环,执行的总次数为2*n+10,其时间复杂度为O(n)。
空间复杂度:该程序从头到尾创建了两个变量,且与n的大小无关,故该程序的空间复杂度为O(1)。
二分查找的时间复杂度和空间复杂度
int Binary_Search(int arr[],int key,int sz)
{
int left = 0;
int mid = 0;
int right = sz-1;
while(left<=right)
{
mid = left + (right-left)/2;
if(arr[mid] == key)
{
return mid;
}
else if(arr[mid] > key)
{
right = mid-1;
}
else
{
left = mid+1;
}
}
return -1;
}
二分查找的基本思想是将n个元素分成大致相等的两部分,取arr[n/2]与key做比较,如果key=arr[n/2]则找到key算法中止;如果key< arr[n/2],则只要在数组arr的左半部分继续搜索key;如果key>arr[n/2],则只要在数组arr的右半部搜索key。
时间复杂度:
时间复杂度无非就是while循环的次数!总共有n个元素,渐渐跟下去就是n,n/2,n/4,….n/2^k(接下来操作元素的剩余个数),其中k就是循环的次数。由于n/2^k取整后>=1,即令n/2^k=1,可得k=log2n,(以2为底,n的对数),所以时间复杂度可以表示为O(log2n)。
空间复杂度:
函数中创建对象的个数为常数级别,与n的变化无关,因此二分查找的空间复杂度为O(1)。
斐波那契数列的时间复杂度和空间复杂度
//递归实现
int fib(int n)
{
if(n<=2)
return 1;
else
return fib(n-1)+fib(n-2);
}
时间复杂度:
斐波那契数列的求取过程如图所示,相当于二叉树,且为非满树。求取其时间复杂度相当于求树的分支,故斐波那契数列递归实现的时间复杂度为O(2n) (2的n次方)。
空间复杂度:
求取斐波那契数列的空间复杂度相当于求递归的深度,即树的深度,故斐波那契数列递归实现的空间复杂度为O(n)。
//非递归
int fib(int n)
{
int pre = 0;
int cur = 0;
int next = 0;
cur = pre = 1;
while(n>2)
{
next = pre+cur;
pre = cur;
cur = next;
n--;
}
return cur;
}
时间复杂度:
斐波那契数列的非递归实现的时间复杂度为while循环的次数,其时间复杂度可表示为O(n)。
空间复杂度:
求取斐波那契数列的非递归实现的空间复杂度,在函数中创建的对象个数为常数级,与n的大小无关,因此其空间复杂度为O(1)。
常见算法的时间复杂度和空间复杂度
总结
- 求普通递归的时间复杂度时,采用递归的方式求取。
- 斐波那契数列的时间复杂度为二叉树的个数;
斐波那契数列的时间复杂度为函数调用栈的次数即二叉树的深度。