目录
1. 数据结构在学什么
常言道:学以致用;学习完基本C语言程序,我们希望通过数据结构的学习:
1. 用程序代码把现实世界的问题信息化。
2. 用计算机高效的处理这些信息从而创造价值。
2. 数据结构的基本概念
数据:数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。(将数据丢给计算机,通过写程序来处理这些数据,进而创造出价值)
数据元素:数据元素是数据项的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单元。例如:学生记录就是一个数据元素,它是由学号、姓名、性别等数据项组成。
数据对象:数据对象是具有相同性质的数据元素的集合。
数据结构:数据结构是相互之间存在的一种或多种特定关系的数据元素的集合。在任何问题中,数据元素都不是孤立存在的,它们之间存在某种关系,这种数据元素相互之间的关系称为结构(Structure)。
数据结构包括三方面内容:逻辑结构、存储结构、数据的运算。数据的逻辑结构和存储结构是密不可分的,算法的设计取决于所采用的逻辑结构,而算法的实现又取决于数据的存储结构。
逻辑结构分为:
集合结构(各个元素同属于一个集合)
线性结构(数据元素一对一的关系)
树形结构
图状结构
存储结构(又称为物理结构):落实到实处,存储结构就是用计算机去实现这种数据结构。---如何在计算机内部表示数据元素的逻辑关系。常见的存储结构有:顺序存储、链式存储、索引存储、散列存储。
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中。元素间的关系由存储单元的邻接关系来体现。
链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
索引存储:在存储元素信息的同时,还建立附加的索引表。
散列存储:根据元素的关键字直接计算出元素的存储地址,又称为哈希存储。
数据的运算:针对于某种逻辑结构,结合实际需求,定义其基本运算。
ag. 一个榜单:其逻辑结构为线性结构;基本运算有:查找第 i 个数据元素、在第i个位置插入新的数据元素、删除第i个位置的数据元素(增删查改)……
3. 算法和算法评价
3.1 算法的基本概念
算法(Algorithm)是对特定问题求解步骤的一种描述,他是指令的有限序列,其中每条指令表示一个或多个操作。
程序=数据结构+算法;
也就是说要写一段程序:数据结构在其中的作用是:如何用数据正确的描述该问题,并且存入计算机。
算法的作用是:如何高效的处理这些数据,以解决实际的问题。
3.2 算法的特征
1. 有穷性:一个算法必须在执行有穷步之后结束,且每一步都可在有穷时间内完成。
2. 确定性:算法的每条指令必须有确切的含义,对于相同的输入必须对应相同的输出。
3. 可行性:算法中描述的操作都可以通过已经实现的基本操作执行有限次来实现。
4. 输入:一个算法有零个或多个输入,这些输入取自某个特定的对象的集合。
5. 输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
通常设计一个好的算法需要保证算法:
1. 能够正确的解决求解问题。
2. 帮助人们更好的理解程序,具有良好的可读性。
3. 当算法被非法输入时,算法能够做出反映和处理,而不会盲目的输出结果。
4. 算法执行的效率和时间要合理。
3.3 算法效率的度量
算法效率的度量是通过时间复杂度和空间复杂度来描述的。
3.3.1 时间复杂度
频度:一个语句在算法中被重复执行的次数称为该语句的频度。比方说:for循环内i++;则i++这条语句循环的次数是for的范围;
算法中所有语句的频度之和记为T(n),它是该算法语句规模n的函数,时间复杂度主要分析T(n)的数量级。算法中基本运算(最深层循环内的语句,也就是循环嵌套最深层次的语句(相当重要))的频度与T(n)同数量级,因此通常采用算法中基本运算的频度f(n)来分析算法的时间复杂度。时间复杂度记为
注:取f(n)中随n增长最快的项,将其的系数置为1作为时间复杂度的度量。例如:f(n)=an^3+bn^2+cn的时间复杂度为O(n^3);
也就是说当问题规模比较复杂的时候,可以舍去其阶数较少的部分,只考虑阶数高的部分。(相当重要)
算法的时间复杂度不仅依赖于问题的规模n,也取决于待输入数据的性质(如输入数据元素的初始状态);取决于输入数据的性质具体是什么意思呢?我们看以下程序:
下述程序是在数组A[0……n-1]中,查找某个给定值k的算法
i=n-1;
while(i>=0 && (A[i]!=k))//i>=0并且下角标i对应的元素不为k时执行i--;
{
i--;
}
return i;
根据上述学习,时间复杂度T(n)和算法中基本运算(最深层次的循环语句)同等级别;本程序中对应的基本算法就是 i--;
i--循环出现的次数,也就是频度取决于第几次能找到k,这显然受初始状态数组A[0……n-1]中n的值影响;
1. 如果数组中没有与k相等的值,那么需要将整个数组循环一次,while循环才会结束,所以此时的时间复杂度为n;
2. 如果A中第 1 个元素是k,那么只需要循环一次就可以找到k,此时的时间复杂度是1;
3. 如果A中最后一个元素是k,那么不需要循环while就可以找到k,因为初始值定义i=n-1,此时的时间复杂度是0;
所以说算法的时间复杂度取决于待输入数据的性质(如输入数据元素的初始状态);
最坏时间复杂度:在最坏的情况下,算法的时间复杂度。
平均时间复杂度:所有可能输入实例在等概率出现的情况下,算法的期望运行时间。
最好时间复杂度:在最好的情况下,算法的时间复杂度。
一般考虑最坏情况下的时间复杂度,保证算法的运行时间不会比它还要长。
分析时间复杂性时,遵循的两条规则:
①加法规则:T(n) = T(n)+T(n)=O((n))+O(g(n)) = O(max(f(n),g(n)) 多项相加,只保留最高阶的项,且系数变为1
②乘法规则:T(n) = T(n)×T(n)=O(f(n))×O(g(n)) = O((n)×g(n) 多项相乘,都保留
常见的渐近时间复杂度:
(相当重要)
需要根据该复杂度排序判断加法规则中究竟保留哪个值;因为加法规则定义多项相加,保留的是最高阶的值;需要根据该排序判断究竟谁才是最高阶的值; 口诀:常对幂指阶 -----常数的时间复杂度要小于对数小于幂指数小于指数小于阶乘。
相关练习:
1. 算法的时间复杂度是O(n^2),表明该算法的 执行时间与n^2成正比。
2. 以下算法的时间复杂度为:
void fun(int n)
{
int i=1;
while(i<=n)
{
i=i*2;
}
}
显然:该程序的基本语句为i=i*2;时间复杂度就是计算i=i*2循环的次数,通过该程序可以发现,要想离开while循环,只需要i>n即可,分析该循环,每循环一次i=i*2;假设循环t次,每次相当于在原有的基础之上乘个2;循环一次,i=2;循环两次,i=4,循环3次,i=8,所以2的t次方(2^t<=n),所以
;
拓展: 计算下述程序的时间复杂度?
for(m=1;m<n;m++)
{
i=1;
while(i<n)
{
i=i*2;
}
}
根据上述的计算,我们已经计算出while循环中的时间复杂度是log以2为底的n次方,大的for循环m=1;m<n,循环次数为n,所以总的时间复杂度为n倍的log以2为底的n次方。由此可以总结:时间复杂度是有其线性特征的;对于嵌套结构同样适用。
3. 以下算法的时间复杂度为:
void fun(int n)
{
int i=0;
while(i*i*i<=n)
{
i++;
}
}
显然,该程序的基本语句为i++,频度也就是i++循环的次数;不妨设i++循环的次数为t,因为i初始值为0,循环t次后i的值为t,所以离开while循环的条件t^3<=n,所以时间复杂度为n开三次方;
4. 计算下述程序的时间复杂度?
for(x=1;i<=n;x++)
{
for(i=1;i<=n;i++)
{
j=1;
j++;
}
}
最深层的for循环作为基本语句,显然i=1到i<=n,循环次数为n,外部for循环的循环次数也为n,所以最终的时间复杂度为T(n)=O(n*n);
在此处,如果把外部的for循环改为i<=m,则最终时间复杂度为T(n)=O(m*n);
5. 问题规模参数不只一个的情况 计算时间复杂度
计算Contract的时间复杂度?
void Contract(int N,int M)
{
int sz=0;
for(k=0;k<N;k++)
{
sz++;
}
for(k=0;k<M;k++)
{
sz++;
}
printf("hehe\n");
return 0;
}
特别注意:答案是:O(N+M);该时间复杂度的计算不同于循环多次嵌套的情况,Contract接收到的参数N和M在函数体内部的地位是同等的,N和M的阶数是一样的,不知道两者对结果哪个影响大;如果题目写明,N远远大于M,则时间复杂度就是O(N);
其实通过数学极限能够更好的来说明这种情况,在数学极限中,对于高阶无穷小的定义,当复杂的因式不容易化简时,我们通常考虑阶数最大的那个,在无穷比阶的过程中,只有阶数最大的因式在起作用,其余的都会消去;
6. 基本语句的频度在最坏情况下的时间复杂度?
//其中n为正整数,则基本语句的频度在最坏情况下是:
for (i = n - 1; i > 1; i++)
{
for (j = 1; j < i; j++)
{
if (A[j]>A[j + 1])
{
A[j]与A[j + 1]对换;
}
}
}
该程序中的基本语句显然是: A[j]与A[j + 1]对换; 频度在最坏的情况的下就是if判断语句永远成立的情况。
所以整体是 一个2层for循环嵌套的情况:
第一层for循环的频度是:i从1到n-1; 第二层的频度是:j从1到i;
所以当 i 等于1时,基本语句的频度是0;因为第二层嵌套是从1到1;
当i=2时,基本语句的频度是1,因为第二层嵌套是从1到2;
依次类推,第i=n-1;第二层是从1到n-1; 所以总的时间复杂度是0+1+2+3+4+……n=n(n-1)/2----保留下来高阶就是 T(n)=O(n^2);
7. 计算m++的执行次数?
int m = 0, i, j;
for (i = 1; i <= n; i++)
{
for (j = 1; j < 2 * i; j++)
{
m++;
}
}
首先: 最外部的for循环i是从1到n的;里面的for循环j是从1到2i的,所以m++首先循环2i次,只不过这个2i次是建立在最外部for循环上的,因此,每当外部for循环一次,内部的2i次就依次变为2*1+2*2+2*3+2*4+……2*n=2(1+2+3+4……+n)=2*(n(n+1)/2)=n(n+1);
8. 下面的程序片段的时间复杂度是?
x = 2;
while (x < n / 2);
x = 2 * x;
该程序的基本语句为x=2*x;初始条件为x=2;离开循环的条件为x>n/2;每次循环在上一个循环的基础之上乘2,也就说2*2*2*2*2*2*2*2乘到什么时候会大于n/2结束循环,所以2^n>n/2;所以时间复杂度T(n)=O(log以2为底的n次方);
9. 求整数n(n>=0)的阶乘的算法如下,其时间复杂度是:
int fact(int n)
{
if (n <= 1)
return 1;
return n*fact(n - 1);
}
读上述程序:其实就是一个递归。一次递归是一次乘法运算,而值为n的情况下递归嵌套n次。时间复杂度就是O(n);
10. 下列程序段的时间复杂度是:
count = 0;
for (k = 1; k <= n; k = k * 2)
{
for (j = 1; j <= n; j++)
{
count++;
}
}
该程序的的基本语句是count++;内部循环n次,每次count++;外部循环是1 2 4 8 16 32 一直到k>n时离开循环,所以2^n>n时结束,循环次数log以2为底的n的对数,所用二层嵌套的时间复杂度T(n)=O(nlog以2为底n的对数);
11. 下列程序段的时间复杂度?
x = 0;
while (n >= (x + 1)*(x + 1))
{
x = x + 1;
}
x从0开始,每次的循环进行加1,循环结束的条件是(x+1)^2>n时结束循环体;假设第k次循环结束程序,那么第k次的判断条件是:(x+1)^2>n;因为x是从0开始的,所以第k次时,x=k-1;也就是(k-1+1)^2>n,所以k等于根号n,也就是时间复杂度等于根号n;
总结:
1. 当存在for循环嵌套的情况时,如果内外for循环的循环次数,也就是频度都是定值,可以直接相乘得到循环次数,也就是时间复杂度。但是当for嵌套循环有一层的循环次数是不断改变的情况,比如说最普遍的冒泡排序,最外层的for循环是固定的,内部for循环根据外部改变,这个时候不能简单的进行相乘,必须整理清除整个执行过程。
3.3.2 空间复杂度
算法的空间复杂度S(n)定义为该算法所耗费的存储空间。它是问题规模n的函数。记为:
S(n) = O(g(n))
一个程序在执行时除了需要存储空间来存放本身所需的指令、常数、变量和输入数据之外,还需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占 空间只取决于问题本身,和算法无关,则只需分析除输入和程序之外的额外空间。
原地工作:算法所需的辅助空间为常量,即O(1);
ag. 计算下述程序的空间复杂度?
void test(int n)
{
int flag[n];
int i;
}
假设int 占4B,则4+4n+4=4n+8 (int n=4个字节、int i=4个字节、flag[n]数组n个元素共4n个字节);所以空间复杂度就是S(n)=O(n);这里和时间复杂度是一样的,只需要关注存储空间大小与问题规模相关的变量即可。4n+8,关注其高阶,并且高阶系数置1;
ag. 计算下述程序的空间复杂度?
void test(int n)
{
int flag[n][n];
int other[n];
int i;
}
S(n)=O(n^2)+O(n)+O(1);根据加法规则,保留其中阶数最高的,所以空间复杂度S(n)=O(n^2);
递归调用带来的内存开销:空间复杂度=递归调用的深度(递归的层数)* 每一次递归的空间复杂度。
ag. 计算下述递归程序的空间复杂度?
void loveYou(int n)//n是问题规模
{
int flag[n];
if(n>1)
{
loveYou(n-1);
}
printf("I love You %d\n",n);
}
int main()
{
loveYou(5);
}
显然if判断语句中为递归调用函数,主函数loveYou传参为5,即外部函数void loveYou接收参数5,传给数组flag[5];在函数调用的过程中,第一级调用,数组参数为flag[1];第二级调用,数组参数为flag[2],第三级调用,数组参数为flag[3],第n级调用,数组参数为flag[n],所以最终空间复杂度为1+2+3+4+……n=1/2n^2+1/2n,所以空间复杂度为O(n^2);
总结:空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一种度量,空间不比时间,根据程序代码运行的先后顺序,先执行的程序使用完的空间可以供后续程序使用,(一句程序执行完以后它所使用的的那块空间就会被释放),前后所使用的的空间是同一块空间。
也可以计算程序中变量的个数,也就是开辟变量占用空间的大小。
ag. int *pf=(int*)malloc((m+1)*sizeof(int)); 表示malloc动态开辟开辟m+1个int型空间,此时该一句程序的空间复杂度就是m+1;
又或者说是: int i=0; 定义一个变量i;该一句程序的空间复杂度就是O(1);表示一个常数个空间;
for(j=0;j<100;j++); for循环中定义变量j;该一句程序的空间复杂度就是O(1);表示一个常数个空间;
所以说也可以去计算程序中变量的大小来表示其空间复杂度。