数据结构笔记---绪论

数据结构笔记

  • 一、绪论
    • 1.数据结中的基本概念
      • 1.1 基本概念和术语
      • 1.2 数据结构三要素
        • 1.数据的逻辑结构
        • 2.数据的存储结构
        • 3.数据的运算
    • 2.算法和算法评价
      • 2.1 算法的基本概念与特性
      • 2.2 算法效率的度量
        • 1. 时间复杂度
        • 2. 空间复杂度

一、绪论

1.数据结中的基本概念

1.1 基本概念和术语

  1. 数据:数据是信息的载体,是描述客观事物属性的数字符以及所有能够输入到计算机中并被计算机识别和处理的符号的集合

  2. 数据元素:数据元素是数据的基本单位,通常作为一个整体来进行考虑和处理。数据元素可由若干个数据项组成。

  3. 数据项:数据项是数据记录中最基本的、不可分的有名数据单位,是具有独立含义的最小标识单位。

  4. 数据对象:数据对象是具有相同性质的数据元素的集合,是数据的一个子集

    举例:某班由若干名学生组成
    其中,001、002等等一整条的数据是数据元素
    每条数据元素中的学号、姓名、性别、年龄是数据项
    男生的数据对象就是表中所有性别为男的集合N={student[001,张三,男,18],student[003,派大汤,男,22]}

学号 姓名 性别 年龄
001 张三 18
002 李四 20
003 派大汤 22
  1. 数据类型:一个值的集合和定义在此集合上的一组操作的总称

    1)原子类型:值不可再分的数据类型,例如int、float

    2)结构类型:值可以再分解为若干成分的数据类型
    或者理解为:一种数据结构 + 定义在这种数据结构上的一组操作。
    例如:数组、结构体

    3)抽象数据类型:抽象数据组织以及与之相关的操作

    抽象数据类型的描述方法(D,S,P)
    D是数据对象,S是D上的关系集,P是对D的基本操作集。

    抽象数据类型的定义方法

    ADT 抽象数据类型{
    数据对象(数据对象的定义)
    数据关系(数据关系的定义)
    基本操作(基本操作的定义)
    }ADT 抽象数据类型名

  2. 数据结构:数据机构是相互之间存在一种或多种特定关系的数据元素集合。数据元素之间的关系叫做结构,数据结构包括三方面的内容:逻辑结构、存储结构、数据的运算。

    数据的逻辑结构和存储结构是密不可分的两方面,算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的的存储结构

1.2 数据结构三要素

1.数据的逻辑结构

逻辑结构是指数据元素之间的逻辑关系。它与数据的存储无关,独立于计算机。

逻辑结构分类图:
数据结构笔记---绪论_第1张图片

2.数据的存储结构

存储结构是指数据结构在计算机中的表示(映像),也称为物理结构,包括数据元素的表示和关系的表示。数据的存储结构是用计算机语言实现的逻辑结构,依赖于计算机语言。数据结构的存储方式主要有顺序存储、链式存储、索引存储、散列存储。

存储方式描述与优缺点对比:

存储方式 具体操作 优点 缺点
顺序存储 把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现 可以实现随机存取,每个元素占用最少的内存空间 只能使用相邻的一整块存储单元,可能产生较多的外部碎片
链式存储 借助指示元素存储地址的指针来表示元素之间的逻辑关系 不会出现碎片现象,能充分利用所有内存单元 每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取
索引存储 在存储元素信息的同时,还建立附加的索引表,索引表的每项称为索引项,其一般形式为(关键字,地址) 检索速度快 附加的索引表额外占用存储空间。增加删除也需要修改索引表,会花费更多时间。
散列存储 根据元素的关键字直接计算出该元素的存储地址,又称哈希存储 检索、增加、删除结点操作都很快 若散列函数不好,则可能出现元素存储单元的冲突,解决冲入会增加时间和空间开销

重要的一点:
数据的逻辑结构独立于存储结构
但是存储结构是依赖于逻辑结构

3.数据的运算
  • 施加在数据上的运算包括运算的定义和运算的实现。
  • 运算的定义是针对逻辑结构,指出运算的功能;
  • 运算的实现是针对存储结构,指出具体的操作步骤;

2.算法和算法评价

2.1 算法的基本概念与特性

算法:对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或者多个操作。

算法的特性:

  1. 有穷性:算法必须总在执行有穷步之后结束,且每一步都要在有穷时间内完成。
  2. 确定性:算法中每条指令都必须有确定的含义,对于相同的输入只能得出相同的输出。
  3. 可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
  4. 输入:一个算法有零个或多个输入,这些输入取自于某个特定对象的集合。
  5. 有穷性:一个算法有一个或多个输出,这些输出与输入有着某种特定关系的量。

算法应达到的目标:

  1. 正确性:能够正确的解决求解问题。
  2. 可读性:帮助人们理解。
  3. 健壮性:输入非法数据(遇到错误时),算法能够适当地做出反应或者进行处理,不会产生莫名其妙的输出结果。
  4. 效率与低存储量需求:效率指算法的执行时间,存储量需求指算法的执行时所需要的最大存储空间。两者都与问题的规模有关。
程序=数据结构+算法

2.2 算法效率的度量

1. 时间复杂度

基本概念:

  • 时间复杂度:探究问题规模n与执行时间的关系。
  • 语句的频度:某条语句被重复执行的次数。
  • 算法中所有语句的频度之和极为T(n),它是该算法问题规模n的函数,时间复杂度主要分析T(n)的数量级。算法的时间复杂度记为:
    T(n)=O(f(n))
  • 最坏时间复杂度:指在最坏的情况下,算法的时间复杂度
  • 平均时间复杂度:指所有可能输入实例在等概率出现的情况下,算法的期望运行时间。
  • 最好时间复杂度:指在最好的情况下,算法的时间复杂度

计算时间复杂度两条规则:

  • a.加法规则:T(n)=T1(n)+T2(n)=O(f(n))+O(f(n))=O(max(f(n),g(n)))
  • b.乘法规则:T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n))

计算时间复杂度三条原则:

  1. 顺序执行的代码只影响常数项,可忽略。
  2. 只需找循环中的一个基本操作,分析它的执行次数与n的关系即可,其它的操作影响n的系数。
  3. 如果有多层嵌套循环,只需关注最深层循环次数即可。

计算方法和步骤:

  1. 找到一个基本操作(最深层循环)

  2. 分析该基本操作的执行次数t与问题规模的关系t=f(n),主要关注循环条件与循环内部对变量的操作。

    ① 如果循环主体的变量参与循环条件的判断,设执行次数为t,找t与n之间的关系
    ② 如果循环主体中的变量与循环条件无关,采用数学归纳法,或者累计循环次数。递归程序一般使用公式进行递推。非递归程序直接累加次数。

  3. t的数量级O(t)就是算法时间复杂度T(n)

关于递归函数的公式:
数据结构笔记---绪论_第2张图片

具体计算:
1.常数阶

void main()
{
	int x=0;
	x++;
	printf("%d",x);
}

基本操作:x++(无循环)
执行次数:1
关系:t=f(1)=1
T(n)=O(1)

2.线性阶

void main()
{
	int n=100;
	for (int i=0;i<n;i++){
		printf("%d\n",i);
	}

}

基本操作:printf(“%d”,n);
执行次数:n
关系:t=f(n)=n
T(n)=O(n)

3.平方阶

void main()
{
	int n=5,x=0;
	for( int i =2; i <= n; ++i ){
		for( int j =2; j <= i - 1; ++j ){
			 ++x;
			 printf("%d",x);
		}  
	}
}

基本操作:printf(“%d”,x);
执行次数:(1)+(1+1)+(1+1+1)+…+(n-2)
= (1+n-2)×(n-2)/2
= (n-1)(n-2)/2
= 1 2 {1} \over {2} 21 n 2 n^2 n2- 3 2 {3} \over {2} 23 n n n+1
关系:t=f( 1 2 {1} \over {2} 21 n 2 n^2 n2- 3 2 {3} \over {2} 23 n n n+1)= n 2 n^2 n2
T(n)=O( n 2 n^2 n2)

4. log ⁡ 2 n \log_2 n log2n

#include 
int fun(int n) 
{
    int i=1;
	while(i<=n){
		i=i*2;
	}
	return i;
}

基本操作:i=i*2;
执行次数:
设执行次数t, 2 t 2^t 2t log ⁡ 2 n \log_2 n log2n
关系:t=f( log ⁡ 2 n \log_2 n log2n)= log ⁡ 2 n \log_2 n log2n
T(n)=O( log ⁡ 2 n \log_2 n log2n)

5. n log ⁡ 2 n n\log_2 n nlog2n

int solve(int left, int right,int num[] )
{
	if(left == right)
		return num[left];

	int mid = (left + right) / 2;
	int lans = solve(left, mid,num);
	int rans = solve(mid + 1, right,num);

	int sum = 0, lmax = num[mid], rmax = num[mid + 1];
	for(int i = mid; i >= left; i--) {
		sum += num[i];
		if(sum > lmax) lmax = sum;
	}
	sum = 0;
	for(int i = mid + 1; i <= right; i++) {
		sum += num[i];
		if(sum > rmax) rmax = sum;
	}

	int ans = lmax + rmax;
	if(lans > ans) ans = lans;
	if(rans > ans) ans = rans;
	return ans;
}

int main(void)
{
	int n;
	int num[100],*p;
	p=num;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
		scanf("%d", &num[i]);

	printf("%d\n", solve(1,n,p));
	return 0;
}

方法1:
基本操作:sum += num[i];

该函数利用递归将数组进行不断的等量分割,直到不可再分。类似于在在外边套了一层
while (i 所以设嵌套次数与n的关系:n= 2 m 2^m 2m,则m= log ⁡ 2 n \log_2 n log2n
每次递归都会使用时间复杂度为O(n)的for循环。
所以有执行次数= n 2 {n} \over {2} 2n*1+ n 4 {n} \over {4} 4n*2+ n 8 {n} \over {8} 8n4+…+ n 2 m {n} \over {2^m} 2mn 2 m − 1 {2}^{m-1} 2m1
= n 2 {n} \over {2} 2n+ n 2 {n} \over {2} 2n+ n 2 {n} \over {2} 2n+…+ n 2 {n} \over {2} 2n (m项)
= n 2 {n} \over {2} 2nm
= n 2 {n} \over {2} 2n
log ⁡ 2 n \log_2 n log2n

关系:t=f( n 2 {n} \over {2} 2n log ⁡ 2 n \log_2 n log2n)=n log ⁡ 2 n \log_2 n log2n

T(n)=O(n* log ⁡ 2 n \log_2 n log2n)

方法2:
找递归关系:T(n)=2T( n 2 {n} \over {2} 2n) +cn//将n除以2,再进行两遍,cn是下面的循环语句,复杂度为线性阶。

T(n)= 2 1 2^1 21T( n 2 1 {n} \over {2^1} 21n)+1cn
T(n)= 2 2 2^2 22T( n 2 2 {n} \over {2^2} 22n)+2cn
T(n)= 2 3 2^3 23T( n 2 3 {n} \over {2^3} 23n)+3cn

T(n)= 2 k 2^k 2kT( n 2 k {n} \over {2^k} 2kn)+kcn
另n= 2 k 2^k 2k,所以k= log ⁡ 2 n \log_2n log2n
可得T(n)=cn+cn log ⁡ 2 n \log_2n log2n
T(n)=O(n log ⁡ 2 n \log_2n log2n)

ps:也可以用上面的公式直接得出答案O(n log ⁡ 1 n \log_1n log1n)

证明:时间复杂度与 log ⁡ \log log中的底数无关,(即O( log ⁡ 2 n \log_2n log2n)=O( log ⁡ 3 n \log_3n log3n))
log ⁡ a n \log_an logan/ log ⁡ b n \log_bn logbn
=( log ⁡ c n \log_cn logcn/ log ⁡ c a \log_ca logca)/( log ⁡ c n \log_cn logcn/ log ⁡ c b \log_cb logcb)
=( log ⁡ c b \log_cb logcb/ log ⁡ c a \log_ca logca)
= log ⁡ a b \log_ab logab

所以,底数只是系数,不影响最终的时间复杂度。

6. n 3 \sqrt[3]{n} 3n

void fun(int n){
	int i=0;
	while (i*i*i<=n){
		i++;
	}
}

基本操作:i++
执行次数:
设执行次数为t,t * t * t<=n,即 t 3 t^3 t3<=n,所以t<= n 1 3 {n}^{1\over3} n31
关系:t=f( n 3 \sqrt[3]{n} 3n )= n 3 \sqrt[3]{n} 3n
T(n)=O( n 3 \sqrt[3]{n} 3n )

7. 2 n 2^n 2n

long aFunc(int n) {    
    if (n <= 1) {        
        return 1;
    } else {        
        return aFunc(n - 1) + aFunc(n - 2);
    }
}

递归算法求斐波那契数列

T(1)=T(1)
T(2)=T(1)+T(0)
T(3)=T(2)+T(1)=T(1)+T(0)+T(1)
T(4)=T(3)+T(2)=T(1)+T(0)+T(1)+T(1)+T(0)
T(5)=T(4)+T(3)=T(1)+T(0)+T(1)+T(1)+T(0)+T(1)+T(0)+T(1)

像是二叉树一样,随着n的增长,T(n)以 2 n 2^n 2n的指数增长速度增长。

T(n)= 2 n 2^n 2n个T(0)或者T(1)相加= 2 n 2^n 2n

常见时间复杂度之间的大小关系:

O(1) < O( log ⁡ 2 n \log_2 n log2n) < O(n) < O(n* log ⁡ 2 n \log_2 n log2n) < O( n 2 n^2 n2) < O( n 3 n^3 n3) < O( 2 n 2^n 2n) < O( n ! n! n!) < O( n n n^n nn)

函数图像:
数据结构笔记---绪论_第3张图片

2. 空间复杂度

基本概念:
空间复杂度:算法的空间复杂度S(n)定义为该算法所消耗的存储空间,是问题规模n的函数,记为S(n)。

S(n)=O(g(n))

原地工作:指算法所需的辅助空间为常量,即为O(1)。

理解:
算法执行所需存储空间:

  • 存储算法本身所占用的存储空间(代码量的多少)
  • 算法的输入输出数据所占用的存储空间(由问题规模决定)
  • 算法在运行过程中临时占用的存储空间

空间复杂度=内存调用的深度。
循环语句中的变量大多只开辟一次,空间复杂度为O(1)
递归算法自己调用自己,并且开辟新的内存,所以通常会增加空间复杂度。

计算空间复杂度两条规则:

  • a.加法规则:T(n)=T1(n)+T2(n)=O(f(n))+O(f(n))=O(max(f(n),g(n)))
  • b.乘法规则:T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n))

常见空间复杂度之间的大小关系:

O(1) < O( log ⁡ 2 n \log_2 n log2n) < O(n) < O(n* log ⁡ 2 n \log_2 n log2n) < O( n 2 n^2 n2) < O( n 3 n^3 n3) < O( 2 n 2^n 2n) < O( n ! n! n!) < O( n n n^n nn)

计算方法和步骤:

  • 普通程序:
    1.找到所占空间大小与问题规模有关的变量
    2.分析所占空间x与问题规模n的关系x=f(n)
    3.x的数量级O(x)就是算法的空间复杂度S(n)

  • 递归程序:
    1.找到递归调用的深度x与问题规模n的关系x=f(n)
    2.x的数量级O(x)就是算法的空间复杂度S(n)
    注意:有的算法各层函数所需要的存储空间不同,方法略有区别。

具体计算:

void fun(int n)
{
	printf("%d", n % 10);
    if(n / 10 != 0)
		fun(n / 10); 
}

int main(void)
{
	fun(10203);
	return 0;
}

开辟空间的语句:void fun(int n)

执行次数:问题规模n的位数就是执行次数

所以可知执行次数t=f( log ⁡ 10 n \log_{10} n log10n)= log ⁡ 10 n \log_{10} n log10n

S(n)=O( log ⁡ 10 n \log_{10} n log10n)

你可能感兴趣的:(数据结构基础,数据结构)