绪论

绪论

知识框架

  • 程序 = 数据结构 + 算法
  • 数据结构:是一门研究非数值计算的程序设计中计算机中的操作对象以及它们之间的关系和操作的学科。
  • 数据结构是相互之间存在的一种或者多种特点关系的数据元素的集合,数据结构由相互之间存着的一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成的

No.1数据结构

一、基本概念和术语

1.数据

  • 数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,是信息的载体,并输入给计算机处理的符号集合。 (数据不仅包含整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。)

2.数据元素

  • 数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。简称为元素,也被称为记录、结点或顶点。。 (比如:鸡鸭牛羊猪狗等动物就是禽类的数据元素。)
  • 与数据的关系:是集合的个体。

3.数据项

  • 数据项:一个数据元素可以由若干个数据项组成。 (比如:人可以有眼耳口鼻这些数据项。)
  • 数据项是数据不可分割的最小单位。
  • 数据项是构成数据元素的最小单位

4.数据对象

  • 数据对象:是性质相同的数据元素的集合,是数据的子集。 (什么是性质相同呢,是指数据元素具有相同数量和类型的数据项,比如:人都有姓名、生日、性别等相同的数据项)
  • 既然数据对象是数据的子集,在实际应用中,处理的数据元素通常具有相同性质,在不产生混淆的情况下,我们都将数据对象简称为数据。

5.数据结构

  • 不同的数据元素之间不是独立的,而是存在特定的关系,我们将这些关系成为结构。
  • 数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。

  1. 这两张表就是数据
  2. 而单独的一张表就称为数据对象,即人员表是一个数据对象,课程表也是一个数据对象。
  3. 而每张表中的每一行就称为数据元素。
  4. 而姓名,性别,身高,课程代号,课程名就称为数据项。
  5. 数据 > 数据元素 > 数据项。

二、逻辑结构和物理结构

1.逻辑结构

  • 逻辑结构:是指数据对象中数据元素之间的相互关系,即从逻辑关系上描述数据,且独立于计算机,与数据的存储无关。

    1. 集合结构:结构中的数据元素之间除“同属一个集合”外,别无其它关系。

    2. 线性结构:结构中的数据元素之间是一对一的关系,除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继。

    3. 树形结构:结构中的数据元素之间存在一对多的的层次关系。

    4. 图形结构:数据元素之间是多对多的关系

2.物理结构(存储结构)

  • 物理结构:是指数据的逻辑结构在计算机中的存储形式。

  • 数据的存储结构主要有:顺序存储、链式存储、索引存储和散列存储。

    1. 顺序存储结构:是把数据存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。

    1. 链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置。

3.逻辑结构与存储结构的联系

  • 逻辑结构是数据结构的抽象,存储结构是数据结构的实现。
  • 逻辑结构:指各数据元素之间的逻辑关系。
  • 存储结构:就是数据的逻辑结构用计算机语言的实现表示,也是逻辑结构的存储映像。

三、抽象类型数据

1.数据类型

  • 数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。

  • 不同的数据类型,开辟出的内存空间也不一样。

  • 数据类型 = 值的集合 + 值集合上的一组操作

  • 在C语言中,按照取值的不同,数据类型可以分为两类:

    1. 原子类型:是不可再分解的基本类型,包括整型、实型、字符型。

    2. 结构类型:由若干个类型组合而成,是可以再分解的。例如,整型数组是由若干个整型数据组成的。

比如,在C语言中变量生命int a,b,这就意味着,在给变量a和b赋值时不能超出int的取值范围,变量a和b之间的运算只能是int类型所允许的运算。

2.抽象数据类型

  • 抽象:从具体事物抽出、概括出它们共同的方案、本质属性与关系等,而将个别的、非本质的方面、属性与关系舍弃,这种思想称为抽象。
  • 抽象数据类型(Abstract Data Type, ADT):是指一个数学模型及定义在改模型上的一组操作。
  • 抽象数据类型的定义仅取决于它的一组逻辑特性,而与其他计算机内部如何表示和实现无关。
  • 抽象数据类型体现了程序设计中的问题分解、抽象和信息隐藏的特性。

# include<stdio.h>

// 定义结构体,表示一个复数
typedef struct {
    double real;  // 实部
    double imag;  // 虚部
} Complex;

// 定义函数,实现对复数的操作
Complex complex_add(Complex a, Complex b) {
    Complex result;
    result.real = a.real + b.real;
    result.imag = a.imag + b.imag;
    return result;
}

Complex complex_sub(Complex a, Complex b) {
    Complex result;
    result.real = a.real - b.real;
    result.imag = a.imag - b.imag;
    return result;
}

Complex complex_mul(Complex a, Complex b) {
    Complex result;
    result.real = a.real * b.real - a.imag * b.imag;
    result.imag = a.real * b.imag + a.imag * b.real;
    return result;
}

// 在主函数中使用复数
int main() {
    Complex a = { 1.0, 2.0 };
    Complex b = { 2.0, 3.0 };
    Complex c = complex_add(a, b);
    Complex d = complex_sub(a, b);
    Complex e = complex_mul(a, b);
    return 0;
}

No.2算法

一、算法定义

  • 算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。

二、算法与程序

  • 算法是解决问题的一种方法或一个过程,考虑如何将输入转换成输出,一个问题可以有多种方法。
  • 程序使用某种程序设计语言对算法的具体实现。
  • 算法根据数据结构来设计程序。
  • 数据结构通过算法来实现操作。

三、算法特性

  • 有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。
  • 确定性:算法中的每一条指令必须有确切的含义,没有二义性,在任何条件下,只有唯一的一条执行路径,即对于相同的输入只能得到相同的输出。
  • 可行性:算法是可执行的,算法描述的操作可以通过已经实现的基本操作执行有限次来实现。
  • 输入:一个算法有零个或多个输入。
  • 输出:一个算法有一个或多个输出。

四、算法设计要求

  • 正确性:算法满足问题需求,能正确解决问题。
  • 可读性:算法应具有良好的可读性,以帮助人们理解。
  • 健壮性:输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名奇妙地输出结果。
  • 高效性:要求花费尽量少的时间和尽量低的存储需求。

五、算法转化为程序后注意点

  • 程序中不含语法错误。
  • 程序对于几组输入数据能够得出满足要求的结果。
  • 程序对于精心选择的、典型、苛刻且带有***难性的几组输入数据能够得出满足要求的结果。
  • 程序对于一切合法的输入数据都能得出满足要求的结果。

通常以第三条注意点作为衡量一个算法是否合格的标准。

六、算法时间复杂度

  • 在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。
    算法的时间复杂度,也就是算法的时间度量,记作:
    T(n) = O(f(n))

  • 算法运行时间 = Σ 每条语句频度 (语句执行的次数)

  • 若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。一般情况下,算法中基本操作重复执行的次数问题规模n的某个函数f(n),算法的时间量度记作T(n)=O(f(n)),它表示随问题规模n的增大而增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度 (O是数量级符号,表示对计算中的实际步数的近似),简称时间复杂度。

  • 语句频度 T(n),又被称为时间频度,指的是该语句重复执行的次数

  • 如何选择时间复杂度:

    1. 最坏时间复杂度:指在最坏情况下,算法的时间复杂度。(n次)
    2. 平均时间复杂度:指在所有可能输入实例在等概率出现的情况下,算法的期望运行时间。(O(n)次)
    3. 最好时间复杂度:指在最好情况下,算法的时间复杂度。(1次)

    (一般总是考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长。)

  • 分析算法时间复杂度的基本方法:

    1. 找出语句频度最大的那条语句作为基本语句
    2. 计算基本语句的频度得到问题规模n的某个函数f(n)。
    3. 忽略所有低次幂项和最高次幂系数,体现出增长率的含义。
    4. 时间复杂度是由嵌套最深层语句的频度决定的。
    5. 取其数量级用符号“O”表示。
  • 对于复杂的算法,可以将它分成几个容易估算的部分,然后利用O(数量级)的加法法则和乘法法则,计算算法时间复杂度数。

    1. 加法法则:T(n) = T1(n) + T2(n) = O(f(n)) + O(g(n)) = O(max(f(n),g(n)))
    2. 乘法法则:T(n) = T1(n) * T2(n) = O(f(n)) * O(g(n)) = O(f(n) * g(n))
  • 算法时间效率比较:当n取得很大时,指数时间算法和多项式时间算法在所需要的时间很悬殊。

  • 时间复杂度,简单来说就是执行某个算法所需要的时间长短,当然越短越好。

  • 时间复杂度T(n)按数量级递增顺序排序:

// 第一题
int i = 1;
int k = 0;
int n = 10;
while(i <= n-1){
	k += 10 * i;  /*计算该语句频度*/
	i++;
}

/*
while 循环了多少次,就是该语句的频度
while 执行一次 i 自增 1 ,当 i>n-1 时退出,就是当 i=n 时退出 while ,i 一开始为 1,所以 while 总共循环了 n-1 次;
频度 :n-1
*/

// 第二题
int i = 1;
int k = 0;
int n = 10;
do{
	k += 10 * i;  /*计算该语句频度*/
	i++;
}while(i <= n-1);

/*
do-while 循环了多少次,就是该语句的频度
循环体执行一次 i 自增 1 ,当 i>n-1 时退出,就是当 i=n 时退出循环体,i 一开始为 1 ,所以循环体总共循环了 n-1 次;
频度 :n-1
*/

// 第三题
int k = 0;
int n = 10;
for(int i = 1; i <= n; i++){
	for(int j = i; j<=n; j++){
		k++;    /*计算该语句频度*/
	}
}

/*
第一层循环 n 次(前提是还未跳出循环,则不是n+1,这是重点!!!)
第二层循环 n+(n-1)+(n-2)+…+2+1 = n(n+1)/2 ,所以 k++ 执行了 n(n+1)/2 次
频度 :n(n+1)/2
*/

// 第四题
int n = 100;
int i = 0;
while(n >= (i+1)*(i+1)){
	i++;    	/*计算该语句频度*/
}

/*
循环体执行 floor(sqrt(n)) 次    // sqrt(n) 是指 n 的平方根,floor(n) 表示向下取整
频度: 根号 n
 */

// 第五题
int x = 0;
int i,j,k;
int n = 10;
for(i = 1; i <= n; i++) {
	for(j = 1; j <= i ; j++) {
		for(k = 1; k <= j; k++) {
			x += 1;   /*计算该语句频度*/
		}
	}
}

/*
第一层 n
第二层 1+2+3+4+…+n
第三层 1+(1+2)+(1+2+3)+(1+2+3+4)+…+(1+2+3+4+…+n) = n(n+1)(2n+4)/12 = n(n+1)(n+2)/6
频度:n(n+1)(n+2)/6
*/

// 第六题
int i = 1;
int j = 0;
int n = 10;
while(i+j <= n){
	if(i > j)/*计算该语句频度*/ j++;
	else i++;
}

/*
注意:if 语句不管真假都会判断一次,所以循环了多少次就判断了多少次 if 语句
当 i+j=n+1 时退出循环
所以循环次数为 (n+1)-1 = n
所以频度为 n
*/

// 第七题
int x = 91;
int y = 100;
while(y > 0){
	if(x > 100){  /*计算该判断语句频度*/
		x -= 10;
		y--;
	}else{
		x++;
	}
}

/*
看 while 循环体中的 if 语句频度就看 while 循环次数
开始 x=91 ,循环了 10 次,每次都执行 else ,直到 x=101
当 x=101,循环了 1 次,if 条件成立,x 又变成了 91 ,而 y=99;
while 循环还没退出之前都是按照这规律循环,直到 y=0 退出 while ,一共重复了 100 遍上面的规律,每次 11 次循环,
所以该语句频度为 100*11 = 1100
*/

// for循环注意点
int k = 1;
for(int i = 1; i <= n ;i++){  //(1)
	k++;     //(2)
}

/*
(1)语句频度是n+1
i 变量在第一个 for 循环中,从取 i = 1 开始执行,直到i=n时为止,至此,i 执行了n次。加上最后i=n+1跳出循环的判断,故,频度共n+1 次;

(2)语句频度是n
当 i = n+1时跳出循环,所以里面的循环体一共执行了 n 次;
*/

七、算法空间复杂度

  • 算法的空间复杂度S(n)通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:
    S(n) = O(f(n))

  • 算法本身要占据的空间有:输入、输出、指令、常数、变量等,以及算法要使用的辅助空间

  • 空间复杂度,简单来说就是执行某个算法所需要开辟的内存空间,当然是越小越好。

  • 空间复杂度的计算的是申请变量的个数,比较常用的有:O(1)、O(n)、O(n²)。

    1. 如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量 O(1)。注意: 如果这段代码里有for循环,但没有再分配新的空间,时间复杂度也不会因为循环而改变。
    2. 举例 int[] m = new int[n]。这段代码中,创建了一个数组出来,这个数据占用的大小为n,因此空间复杂度为 O(n)。和时间复杂度一样,如果加一个循环在这行代码前面,空间复杂度变为 O(n²)。
// 将一维数组a中的N个数逆序放在原数组中。

// 算法一:通过交换首尾数组元素   O(1) -> 没有占用辅助空间!
# include <stdio.h>
# define N 10

int main() {
	int i;
	int a[N] = { 1,2,3,4,5,6,7,8,9,10 };
	int t;
	for ( i = 0; i < N/2; i++)
	{
		t = a[i];
		a[i] = a[N - i - 1];
		a[N - i - 1] = t;
	}

	for ( i = 0; i < N; i++)
	{
		printf("%d %d\t",i, a[i]);
		printf("\n");
	}

	return 0;
}

// 算法二:通过将数组a中的元素逆序放在数组b中,再将数组b的元素正序放回数组a中   O(n)
# include<stdio.h>
# define N 10

int main() {
	int i;
	int a[N] = { 1,2,3,4,5,6,7,8,9,10 };
	int b[N] = { 0 };
	for ( i = 0; i < N; i++)
	{
		b[i] = a[N - i - 1];
	}

	for ( i = 0; i < N; i++)
	{
		a[i] = b[i];
		printf("%d %d", i, b[i]);
		printf("\n");
	}


	return 0;

}

八、数据的运算

  • 数据运算是指对数据实施的操作,数据运算最终需要在对应的存储结构上用算法实现,所以数据运算分为运算定义和运算实现两个层面。
  • 运算定义:是运算功能的描述,是抽象的,是基于逻辑结构的。
  • 运算实现:是程序员完成运算的实现算法,是具体的,是基于存储结构的。
  • 逻辑结构、存储结构、运算三者之间的关系如下:
    1. 存储结构是逻辑关系的映像与元素本身的映像。
    2. 逻辑结构是数据结构的抽象,存储结构是数据结构的实现。
    3. 运算实现是基于存储结构的。

posted @ 2023-08-05 15:44  Ghost3  阅读(15)  评论(0编辑  收藏  举报