黑白之海
当指针转向天台

基础理论 | 数据结构知识点整理

在?看看代码。

听的是b站青岛大学王卓老师的课。系列第一个视频放下面了。

涉及的语言是类C语言,没真的跑过。

绪论

基本概念与术语

数据(Data)

是能输入计算机且能被计算机处理的 各种符号的集合

  • 信息的载体
  • 对客观事物符号化的表示
  • 能够被计算机识别、储存和加工 包括:
  • 数值型的数据:整数、实数等
  • 非数值型的数据:文字、图像、图形、声音等

数据元素(Data Element)

数据的 基本单位 ,在计算机程序中通常作为一个整体进行考虑和处理。

也简称为元素,或称为记录、结点或顶点。

如一张表中,某个行中的所有信息就是数据元素。


数据项

构成数据元素的不可分割的 最小单位

如一行中某一格就是数据项。

数据 > 数据元素 > 数据项


数据对象(Data Object)

性质相同的数据元素的集合 ,是数据是一个子集。

整数数据对象是集合N={0, ±1, ±2, … }

字母字符数据对象是集合C={‘A’, ‘B’, ‘C’, … , ‘Z’ }

学籍表也可以看作一个数据对象


数据元素与数据对象

数据元素——组成数据的基本单位

与数据的关系:数据元素是集合的个体

数据对象——性质相同的数据元素的集合

与数据的关系:数据对象是集合的子集


数据结构(Date Structure)

数据元素不是孤立存在的,它们之间存在着某种关系, 数据元素相互之间的关系称为结构 (Structure)

数据结构是指, 相互之间存在一种或者多种特定关系 的数据元素集合。或者说,数据结构是 带结构 的数据元素的集合。

数据结构包括以下三个方面的内容:

  1. 数据元素之间的逻辑关系,也称为逻辑结构
  2. 数据元素及其关系在计算机内存中的表示(又称为映像),称为数据的物理结构或者数据的存储结构
  3. 数据的运算和实现,即对数据元素可以施加的操作以及这些操作在相应的存储结构上的实现。

数据结构的两个层次

逻辑结构:

  • 描述数据元素之间的逻辑关系
  • 与数据的存储无关,独立于计算机
  • 是从具体问题抽象出来的数学模型

物理结构/存储结构:

  • 数据元素及其关系在计算机存储器中的结构(存储方式)
  • 是数据结构在计算机中的表示

逻辑结构与存储结构的关系:存储结构是逻辑关系的映象与元素本身的映象。逻辑结构是数据结构的抽象,存储结构是数据结构的实现。两者综合起来建立了数据元素之间的结构关系。


逻辑结构的种类

划分方法一:

  1. 线性结构(一对一的):有且仅有一个开始和一个终端结点,并且所有结点都最多只有一个直接前趋和一个直接后续。例如:线性表、栈、队列、串。
  2. 非线性结构(一对多,多对多):一个结点可能有多个直接前趋和直接后续。例如:树、图。

划分方法二:

四种基本逻辑结构

  1. 集合结构:结构中的数据元素之间除了同属于一个集合的关系外,无任何其他关系。
  2. 线性结构:结构中的数据元素之间存在着一对一的的线性关系。
  3. 树形结构:结构中的数据元素之间存在着一对多的层次关系。
  4. 图状结构:结构中的数据元素之间存在着多对多的任意关系。

存储结构的种类

四种基本的存储结构:

  1. 顺序存储结构:用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置来表示。C语言中用数组来实现顺序存储结构。
  2. 链式存储结构:用一组任意的存储单元存储数据元素,数据元素之间的逻辑关系用指针来表示。C语言中用指针(指针就是地址,存储每一个元素本身,并同时存储了后面元素的地址)来实现链式存储结构。
  3. 索引存储结构:在存储结点信息的同时,还建立附加的索引表。
  4. 散列存储结构:根据结点的关键字直接计算出该结点的存储地址。

数据类型

  • int
  • float
  • string
  • 等等

数据类型的作用:约束变量或常量的取值范围,约束变量或常量的操作。


抽象数据类型的表示和实现

抽象数据类型(Abstract Data Type, ADT)

是指一个数学模型以及定义在此数学模型上的一组操作。

  • 由用户定义,从问题抽象出数据模型(逻辑结构)
  • 还包括定义在数据模型上的一组抽象运算(相关操作)
  • 不考虑计算机内的具体存储结构与运算的具体实现算法

抽象数据类型的形式定义

抽象数据类型可用(D, S, P)三元组表示。其中:

D是数据对象;

S是D上的关系集;

P是对D的基本操作集。

抽象数据类型的定义格式

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

其中

  • 数据对象、数据关系的定义用伪代码描述。
  • 基本操作的定义格式为:
    • 基本操作名(参数表)
    • 初始条件:<初始条件描述>
    • 操作结果:<操作结果描述>

基本操作定义格式说明:

参数表:

赋值参数 只为操作提供输入值。

引用参数 以&打头,除可提供输入值外,还将返回操作结果。

初始条件:描述操作执行之前的数据结构和参数应满足的条件,若不满足,则操作失败,并返回相应出错信息。若初始条件为空,则省略之。

操作结果:说明操作正常完成之后,数据结构的变化状况和应返回的结果。

ADT Circle{
       数据对象: D = {r, x, y | r, x, y 均为实数}
       数据关系: R = { <r, x, y> | r是半径, <x, y>是圆心坐标}
       基本操作:
       Circle(&C, r, x, y)
                        操作结果: 构造一个圆。
        double Area(C)
                        初始条件: 圆已存在。   
                        操作结果: 计算面积。
        double Circumstance (C)
                        初始条件: 圆已存在。
                        操作结果: 计算周长。             
} ADT Circle


ADT Complex{
	D = { r1, r2, | r1, r2都是实数}
	S = { <r1, r2> | r1是实部,r2是虚部}
	assign(&C, v1, v2)
		初始条件: 空的复数C已存在
		操作结果: 构造复数C, r1, r2分别被赋予以参数v1, v2的值
	destroy(&C)	
		初始条件: 复数C已存在
		操作结果: 复数C被销毁
	getReal(C, &realPart)
		初始条件: 复数C已存在
		操作结果: 用realPrat返回复数C的实部值
	getImag(C, &imagPart)
			初始条件: 复数C已存在
			操作结果: 用imagPrat返回复数C的虚部值
	add(c1, c2, &sum)
		初始条件: c1, c2是复数
		操作结果: sum返回两个复数c1, c2的和
	
} ADT Complex

然后到了用C实现的部分我发现我一点都不记得C了(扶额)谁用C写程序啊(摔)

    typedef struct complex{
		float real;
		float imag;
	};

typedef struct{
	float real;
	float imag;
}COMPLEX;

我白纸到去搜了这两种写法有什么不同What is the difference between these two structure declarations?

写在struct(结构体)后面的,是tag name。我们可以反复使用这个tag name来定义新的变量,写法为struct complex c1;。看帖子说如果是C++可以直接用complex c1;

如果只写在结构体结尾花括号后,则是给这个没有tag name的结构体别名(aliases)为新的结构体类型(type),叫COMPLEX。定义新的变量时,用COMPLEX c2;

按视频流程,下面采用只定义结构体类型的写法:

typedef struct{
	float real;
	float imag;
}Complex;

继续声明涉及到的函数。

void assign(Complex*A,float real,float imag); /* 赋值 */
void add(Complex*c,Complex A, Complex B);
void minus(Complex*c,Complex A, Complex B);
void multipy(Complex*c,Complex A, Complex B);
void divide(Complex*c,Complex A, Complex B);

赋值的函数。

void assign(Complex*A,float real,float imag)
{
	A->realpart=real;/* 实部赋值 */
	A->imagpart=imag;/* 虚部赋值 */
}

求和的函数。

void add(Complex*c,Complex A, Complex B)
{
	c->realpart=A.realpart+B.realpart;/* 实部相加 */
	c->imagpart=A.imagpart+B.imagpart;/* 虚部相加 */
}

在这里,Complex是我们定义的一个结构体类型。带*的是指针变量,是指向Complex类型的指针,不带*的是Complex类型的普通变量。

具体运算的写法: 求值,复数8+6i与复数4+3i的积除以两者的和。

#include <stdio.h>

typedef struct{
	float realpart;
	float imagpart;
}Complex;

void assign(Complex*A,float real,float imag); /* 赋值 */
void add(Complex*c,Complex A, Complex B);
void minus(Complex*c,Complex A, Complex B);
void multipy(Complex*c,Complex A, Complex B);
void divide(Complex*c,Complex A, Complex B);

void assign(Complex*A,float real,float imag);
{
	A->realpart=real;/* 实部赋值 */
	A->imagpart=imag;/* 虚部赋值 */
}
void add(Complex*c,Complex A, Complex B)
{
	c->realpart=A.realpart+B.realpart;/* 实部相加 */
	c->imagpart=A.imagpart+B.imagpart;/* 虚部相加 */
}
void minus(Complex*c,Complex A, Complex B)
{
	c->realpart=A.realpart-B.realpart;
	c->imagpart=A.imagpart-B.imagpart;
}
void multipy(Complex*c,Complex A, Complex B);
{
	c->realpart=A.realpart*B.realpart;
	c->imagpart=A.imagpart*B.imagpart;
}
void divide(Complex*c,Complex A, Complex B);
{
	c->realpart=A.realpart/B.realpart;
	c->imagpart=A.imagpart/B.imagpart;
}

void main()
{
	Complex z1, z2, z3, z4, z4;
	float RealPart, ImagPart;
	assign(&z1, 8.0, 6.0);
	assign(&z2, 4.0, 3.0);
	add(&z3, z1, z2);
	multipy(&z4, z1, z2);
	/* 最后这一步说进行除法时候要判断一下除数是否为零,除数是刚刚算出来的,赋值到z3
	(其实人家也不太确定是不是这样写呢远目,同学们课后研究^^) */
	if(z3.realpart!==0||z3.imagpart!==0){
		divide(&z, z4, z3);
		GetReal(z, RealPart);
		GetImag(z, ImagPart);
	}
	else
	return 0;
}

算法与算法分析

算法的定义

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

简而言之,算法就是解决问题的方法和步骤


算法的描述

  • 自然语言:中英文等
  • 流程图:传统流程图、NS流程图
  • 伪代码、类语言
  • 程序代码

算法与程序

算法是解决问题的一种方法或一个过程,考虑如何将输入转换成输出,一个问题可以有多种算法。

程序是用某种程序设计语言对算法的具体实现。

程序=数据结构+算法

数据结构通过算法实现操作;算法根据数据结构设计程序。


算法的特性

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

算法设计的要求

  • 正确性(correctness)
  • 可读性(readability)
  • 健壮性/鲁棒性(robustness)
  • 高效性(efficiency)

算法的效率

  • 时间效率:算法耗费的时间
  • 空间效率:算法执行过程中所耗费的存储空间

有时候两者是矛盾冲突的。

算法时间效率的度量

两种度量方法:

  • 事后统计(实现后测算)
  • 事前分析(估算方法)

事前分析方法:一个算法的运行时间是指一个算法在计算机上运行所耗费的时间大致可以等于计算机执行一种简单的操作(如赋值、比较、移动等)所需的时间与算法中进行的简单操作次数乘积。也即算法中每条语句(语句频度)的执行时间之和。

算法运行时间=Σ语句频度×该语句执行一次所需的时间。

因为每条语句执行一次的时间,一般只受机器本身软硬件环境影响,与算法无关,所以我们可以假设每条语句执行一次的时间为单位时间,此时,对算法的运行时间的讨论,就可以转化为该算法中,所有语句的执行次数,即频度之和了。

例.

两个n×n矩阵相乘的算法可以描述为:

for(i=1;i<=n;i++)
	for(j=1;j<=n;j++){ // 用花括号括起来的表示这一部分都是循环体 
		c[i][j]=0;   // for循环的子语句只有一行的时候可以不加花括号,下同
		for(k=0;k<n;K++)
		c[i][j]=c[i][j]+a[i][k]*b[k][j];
	}

好到这里我们(谁们)先复习一下for循环

for(init;condition;increment)
{
   statement(s);
}

首先,第一行for后面一般有三个值(有时候除了条件都可以留空,只写一个;即可)。花括号里的是循环体。是反复执行的主体。initinitialzation的意思,给定的初始值。condition是判断条件,如果为真,就继续执行循环体,非真就不执行循环体,并且跳到for循环下一条语句。执行完一遍循环体,会跳到increment,此处一般是更新循环变量的值,值变更后继续判断条件,不断重复,直到条件为假。

再回到上面的例题,这里共有三个for循环。

for(i=1;i<=n;i++) /* 直到i>n,程序都会一直执行此条语句
(i=n+1,即第一次i>n时,也执行,条件判断为假,于是终止for循环),所以这一句一共执行n+1次。
而这一句条件为真的n次,都进入到了下一步,循环主体。
因此内层嵌套的循环主体最终执行次数都要乘以外层循环执行次数的n */
	for(j=1;j<=n;j++){ /* 第二层循环,本身也是执行了n+1次
	但因为这层是外面循环的主体,所以要乘以循环进行的次数n,共计n(n+1)次。
    进入循环体的次数应为n×n次 */
		c[i][j]=0; /*上面一句的循环体,主体执行次数即n×n次。 */
		for(k=0;k<n;k++) /* 同样是循环体部分。
		注意k从0开始,k<n, 单次循环内执行也是n+1次。共计n×n×(n+1)次。
        进入循环体的次数为n×n×n次。 */
		c[i][j]=c[i][j]+a[i][k]*b[k][j]; /* 最后一层的循环体,共计执行n×n×n次。 */
	}

上述算法的时间消耗为$$T(n) = n+1 + n(n+1) + n×n + n×n×(n+1) + n×n×n = 2n^3 + 3n^2 +2n+1$$


算法时间复杂度的渐进表示法

为了便于比较不同算法的时间效率,我们仅比较它们的数量级。

$$T_1(n) = 10n^2$$ $$T_2(n) = 5n^3$$

若有某个辅助函数$f(n)$, 使得当$n$趋近于无穷大时,$T(n)/f(n)$的极限值为不等于零的常数,则称$f(n)$是$T(n)$的同数量级函数。记作$T(n)=O(f(n))$, 称$O(f(n))$为算法的渐进时间复杂度($O$是数量级的符号,$Order$),简称为时间复杂度。

对于刚刚的求解矩阵相乘的问题,算法耗费时间 $$T(n) = 2n^3 + 3n^2 + 2n+1$$

$n→∞$时,$T(n) / n^3 → 2$

这表示$n$充分大时,$T(n)$与$n^3$是同阶或同数量级,引入大"$O$“记号,则$T(n)$可记作

$$T(n) = O(n^3)$$


算法时间复杂度定义

算法中基本语句重复执行的次数问题规模n的某个函数$f(n)$, 算法的时间量度记作:$$T(n)=O(f(n))$$

它表示随着$n$的增大,算法执行的时间的增长率和$f(n)$的增长率相同,称渐进时间复杂度。

基本语句是指哪种语句捏?

  • 算法中重复执行次数和算法的执行时间成正比的语句
  • 对算法运行时间贡献最大
  • 执行次数最多

问题规模$n$指?$n$越大算法的执行时间越长。

  • 排序:$n$为记录数
  • 矩阵:$n$为矩阵的阶数
  • 多项式:$n$为多项式的项数
  • 集合:$n$为元素个数
  • 树:$n$为树的结点个数
  • 图:$n$为图的顶点数或边数

例.

x=0;
y=0;
for(int k=0;k<n;k++) // n+1次
	x++; // 进循环体n次
for(int i=0;i<n;i++) // n+1次
	for(int j=0;j<n;j++) //循环体部分,n次;本身n+1次。共计n(n+1)次
		y++; // 循环体部分,上面for条件中n+1的最后一次没有进入循环体,所以是n×n次

时间复杂度为$$T(n)=O(n^2)$$


例.

void exam(float x[][], int m, int n){
	float sum[];
	for(int i=0;i<m;i++){
		sum[i]=0.0;
		for(int j=0;j<n;j++)
			sum[i] += x[i][j]; // 时间复杂度是由嵌套最深层语句的频度决定的
	}
	for(i=0;i<m;i++)
		cout << i << '':'' << sum[i] << endl;
	
}

时间复杂度为$$T(n)=O(m×n)$$


这里回顾一下各种运算符以及i++++i的区别好了。

i++++i的区别有两个:

  1. i++ 是先赋值,再自身运算加一。++i是先自己加一,再赋值。
  2. i++ 不能作为左值,而++i 可以。

左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址。

int i = 0;
int *p1 = &(++i); //正确
int *p2 = &(i++); //错误

++i = 1; //正确
i++ = 5; //错误
// 例1
int i = 1;
i = i++; 
printf("i = %d",i); 

// 例2
int i = 1; 
int j = (2 * i++) + i;
printf("j = %d",j);

// 例3
int i = 1; 
int j = i + (2 * i++);
printf("j = %d",j);

// 例4
int i = 1;
int j = 1;
int k = i++ + ++i + ++j + j++; 
printf("k = %d",k);

点击展开:答案 1 4 3 8
点击展开:解析 啊我第一次就做对了感觉不要解析欸…… 好的就是严格执行从左到右的运算顺序,i++是先赋值再自增,++i是先自增再赋值。

难一点的!

// 例5
int i=1;
printf ("%d %d %d", i,i++, ++i)

Quora原帖


回到最初两个n×n矩阵相乘的算法的例子:

for(i=1;i<=n;i++)
	for(j=1;j<=n;j++){ 
		c[i][j]=0;   
		for(k=0;k<n;K++)
		c[i][j]=c[i][j]+a[i][k]*b[k][j];// 嵌套最深
	}

$$T(n)=\sum_{i=1}^n \sum_{j=1}^n \sum_{k=1}^n 1 =\sum_{i=1}^n \sum_{j=1}^n n= \sum_{i=1}^n n^2 = n^3= O(n^3) $$


例.

for(i=1;i<=n;i++)
	for(j=1;j<=i;j++)
		for(k=1;k<=j;k++)
			x=x+1; // 嵌套最深

$$T(n)=\sum_{i=1}^n \sum_{j=1}^i \sum_{k=1}^j 1 =\sum_{i=1}^n \sum_{j=1}^i j= \sum_{i=1}^n \frac{i(i+1)}{2} = \frac{1}{2}(\sum_{i=1}^n i^2 + \sum_{i=1}^n i)=\frac{1}{2} (\frac{n(n+1)(n+2)}{6}+\frac{n(n+1)}{2}=\frac{n(n+1)(n+2)}{6}=n^3= O(n^3) $$


例.

i=1;
while(i<=n)
	i=i*2; // 基本语句

若循环执行1次:i=1×2=$2^1$

若循环执行2次:i=2×2=$2^2$

若循环执行3次:i=2×2×2=$2^3$

若循环执行x次:i=$2^x$

设该最内层的基本语句执行$x$次,由循环条件$i<=n$可知,$2^x <=n$ $$∴ x <= \log_2 n$$ $$T(n)=\log_2 n= \lg n $$

老师说对数阶可以都看作是$lg n$,可能说的是比较的时候?算具体数值的时候应该还是写初始答案。


部分情况中,算法基本操作重复执行的次数还随问题的输入数据集不同而不同。

例.

顺序查找,在数组a[i]中查找值等于e的元素,返回其所在位置。

for(i=0;i<=n;i++)
	if(a[i]==e) return i+1; //找到,则返回是第几个元素
return 0;

最好时间复杂度:1次

最坏时间复杂度:n次(一般考虑最坏的情况,最长的运算时间)

平均时间复杂度(后续查找章节再讨论具体算法):$O(n)$


对于复杂的算法,可以将它分为几个容易估算的部分,然后利用加法法则和乘法法则,计算算法的时间复杂度。

加法法则:$$T(n)=T_1(n)+T_2(n)=O(f(n))+O(g(n))=O(\max (f(n),g(n))$$

乘法法则:$$T(n)=T_1(n)×T_2(n)=O(f(n))×O(g(n))=O((f(n)×g(n))$$


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

常数阶 对数阶 线性阶 线性对数阶 平方阶 立方阶 …… K次方阶 指数阶
$O(1)$ $O(\log_2 n)$ $O(n)$ $O(n\log_2 n)$ $O(n^2)$ $O(n^3)$ …… $O(n^k)$ $O(2^n)$

渐进空间复杂度

记作:$$S(n)=O(f(n))$$ 其中。$n$为问题的规模(或者大小)

算法要占据的空间分为:

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

例.

将一维数组a中的n个数逆序存放到原数组中。

算法1:

for(i=0;i<2/n;i++)
	t=a[i];
	a[i]=a[n-i-i];
	a[n-i-1]=t; // 这里临时的t变量就是另外的辅助空间
	

空间复杂度只涉及到1个变量t,所以 $$S(n)=O(1)$$

算法2:

for(i=0;i<n;i++)
	b[i]=a[n-i-1]; // 存到临时的数组b
for(i=0;i<n;i++)
	a[i]=b[i]; // 因为要求存在原数组,所以把数组b按顺序搬回到数组a
	

空间复杂度只涉及到1个数组,数组有多长就需要多长的辅助空间,所以 $$S(n)=O(n)$$

线性表

线性表的定义和特点

线性表(Linear List)

由$n(n>=0)$个数据元素(结点)$(a_1, a_2, … , a_n )$组成的有限序列。

下标是元素的序号,表示元素在表中的位置。$n$为元素总个数,即表长。$n=0$时,称为空表。非空的线性表记作$(a_1, a_2, … , a_n )$。这里的数据元素$a_i(1<=i<=n)$只是一个抽象的符号,其具体含义在不同情况下可以不同。

同一线性表的元素必定具有相同特性,数据元素之间的关系是线性关系。

非空的线性表中,其中任意一个数据元素$a_i$,它的前一个数据元素,我们称为$a_i$的直接前趋,它的后一个数据元素我们称为$a_i$的直接后继。而第一个元素$a_1$,称为线性起点/起始结点,它没有直接前趋,而仅有一个直接后继$a_2$。最后一个元素$a_n$,称为线性终点/终端结点。它没有直接后继,而仅有一个直接前趋$a_{n-1}$。

用线性表表示多项式

如果$a_i$中的$i$刚好能和多项式的指数幂数字对应,则线性表只需记录每一项的系数。

线性表$P=(p_0, p_1, … , p_n)$

稀疏多项式:

$a_i$中的$i$不能和多项式的指数幂数字对应,则需要再记录每一项的指数。

线性表$P=((p_0, e_0), (p_1,e_1), … , (p_m,e_m)$


线性表来表示多项式的运算

现有线性表A=((7,0),(3,1),(9,8),(5,17))

线性表B=((8,1),(22,7),(-9,8))

求和。

创建一个新数组C, 分别从头遍历比较AB中的每一项。

  • 指数相同,对应系数相加,若其和不为零,则在C中增加一个新项。
  • 指数不同,则依次先将指数较小的项复制到C中的对应项,写上系数与指数。
  • 一个多项式已遍历完毕时,将另一个剩余项依次复制到C中即可。

顺序存储相对不灵活,可能造成空间浪费,也可能不够。链表更方便。

线性表的类型定义

ADT List{
	数据对象: D = {a_i | a_i属于Element, (i=1,2,..,n,n>=0)}
	数据关系: R = {<a_(i-1), a_i> | a_(i-1), a_i属于D,(i=2,3,...,n)}
	基本操作:
	InitList(&L)
	操作结果:构造一个空的线性表L
	Destroy(&L)
	初始条件:线性表L已经存在
	操作结果:销毁线性表
	ClearList(&L)
	初始条件:线性表L已经存在
	操作结果:将线性表L重置为空表
	ListEmpty(L)
	初始条件:线性表L已经存在
	操作结果:若线性表L为空表,则返回True 否则返回False
	ListLenth(L)
	初始条件:线性表L已经存在
	操作结果:返回线性表L中的数据元素个数
	GetElem(L,i,&e)
	初始条件:线性表L已经存在,1<=i<=ListLenth(L)
	操作结果:用e返回线性表L中第i个数据元素的值
	LocateElem(L,e,compare())
	初始条件:线性表L已经存在,compare()是数据元素判定函数
	操作结果:返回L中第一个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回值0
	PriorElem(L,cur_e,&pre_e)
	初始条件:线性表L已经存在
	操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前趋,否则操作失败pre_e无意义
	NextElem(L,cur_e,&next_e)
	初始条件:线性表L已经存在
	操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,否则操作失败next_e无意义
	ListInsert(&L,i,e)
	初始条件:线性表L已经存在,1<=i<=ListLenth(L)+1
	操作结果: 在L的第i个位置之前插入新的数据元素e, L的长度加一
	ListDelete(&L,i,&e)
	初始条件:线性表L已经存在,1<=i<=ListLenth(L)
	操作结果: 删除L的第i个位置的数据元素,并用e返回其值, L的长度减一
	ListTraverse(&L,visited())
	初始条件:线性表L已经存在
	操作结果:依次对线性表中每个元素调用visited()
}ADT List

线性表的顺序表示和实现

线性表的顺序存储表示

线性表的顺序表示又称为顺序存储结构或顺序映像。

顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。(中间不存在空的存储单元,必须占用连续的存储空间)

线性表的第一个数据元素$a_1$的存储位置,称作线性表的起始位置或者基地址


顺序表中元素存储位置的计算

如果每个元素占用8个存储单元,$a_i$存储位置是2000单元,则$a_{i+1}$存储位置是? 2008单元

假设线性表的每个元素需占$l$个存储单元,则第i+1个数据元素的存储位置和第i个数据元素的存储位置之间满足关系:

$$LOC(a_{i+1})=LOC(a_i)+l$$

由此,所有数据元素的存储位置均可由第一个数据元素的存储位置得到: $$LOC(a_i)=LOC(a_1)+(i-1)×l$$

顺序表的特点:

  • 以物理位置相邻表示逻辑关系
  • 任一元素均可随机存取(优点)

重新概括一下特点:

  • 地址连续
  • 依次存放
  • 随机存取
  • 类型相同

以上特点都与数组这种数据类型的特点非常类似。因此我们可以用数组来表示某一特定顺序表。

两者的区别,在于线性表长可变,而数组长度不可以动态定义。

在顺序表中,可以用变量来表示顺序表的长度属性。

这里给出一份定义线性表结构体的模板,每次根据题型微调修改即可。

#define LIST_INIT_SIZE 100  //线性表存储空间的初始分配量
  typedef struct{
    ElemType elem[LIST_INIT_SIZE]; /*此处ElemType elem就是需要自己代换的
    比如换成char a等,可以放复数个*/
    int length; //当前长度
  }SqList;

多项式的顺序存储结构类型定义

$$P_n(x)=p_1 x^{e_1}+p_2 x^{e_2}+…+p_m x^{e_m}$$

该多项式用线性表表示:

$$P=((p_1, e_1),(p_2, e_2),…,(p_m,e_m))$$

#define MAXSIZE 1000 //多项式可能达到的最大长度

typedef struct { //这一部分,先定义了多项式非零项的结构体
	float p; //系数
	int e; //指数
}Polynomial;

typedef struct { //接下来才是定义对应的线性表
	Polynomial *elem; //存储空间的基地址
	int length; //多项式中当前项的个数
}SqList; //多项式的顺序存储结构类型为SqList

类C语言的补充

typedef struct{
    ElemType data[MaxSize]; //数组静态分配
    int length; 
  }SqList;

C语言的内存动态分配

需要加载头文件#include <stdlib.h>

typedef struct{
    ElemType *data; //数组动态分配
    int length; 
  }SqList;
  
  SqList L;
  L.Date=(ElemType*)malloc(sizeof(ElemType)*MaxSize) //分配空间
  
	/* malloc(m)函数:开辟m字节长度的地址空间,并返回这段空间的首地址*/
	/* sizeoof(x)运算:计算变量x的长度*/
	/* free(p)函数:释放指针p所指变量的存储空间,即彻底删除一个变量*/

C++的动态存储分配

new 类型名T (初值列表)

功能:申请用于存放T类型对象的内存空间,并依初值列表赋以初值结果值;成功:T类型的指针,指向新分配的内存;失败:0(null)

int *p1 = new int;
//或 
int *p1 = new int(10);

delete 指针p

功能:释放指针p所指向的内存。p必须是new操作的返回值。


C++中参数传递

函数调用时传送给形参的实参必须与形参三个一致(类型、个数、顺序,都一致)

好像又需要回顾一下参数部分,什么是形参?什么是实参?

点击展开:锵锵锵锵!请看! 回头补!

参数传递有两种方法:

  • 传值方式(参数为整型、实数型、字符型等,因为仅仅传递值到形参中,当形参改变,实参的值是不会发生改变的)
  • 传地址(参数为指针变量、引用类型、数组名,实参将地址传递给形参,两者共用同一块空间,形参改变了实参也就改变了)

传值方式

#include <iosstream.h>
void swap(float m, float n)
{
	float temp;
	temp = m;
	m = n;
	n = temp; // 交换了m和n的值
}

void main()
{
	float a,b;
	cin>>a>>b; //从键盘输入值赋给a和b
	swap(a,b); // 将a的值传递给m, 将b的值传递给n, m和n就是形参
	cout<<a<<endl<<b<<endl; // cout是输出的意思。此处形参发生变化,实参没有发生变化,ab的值不变
}

传地址方式——指针变量作参数

形参变化影响实参

先补课C++ 指针运算符

&取地址运算符&var 读作"var 的地址"。*间接寻址运算符,返回操作数所指定地址的变量的值。是指针变量的内容。

#include <iosstream.h>
void swap(float *m, float *n) // 这里的*是定义指针用的
{
	float temp;
	temp = *m; // m指针变量所指的内容
	*m = *n;
	*n = temp; // 交换了m指针所指的变量和n指针所指的变量,也就是a和b的内容交换,ab发生变化
	// 执行完形参mn就被释放了,没了
}

void main()
{
	float a,b,*p1,*p2;
	cin>>a>>b; //从键盘输入值赋给a和b
	p1=&a; // p1指针指向a的地址
	p2=&b; //p2指针指向b的地址
	swap(p1,p2); 
	cout<<a<<endl<<b<<endl;
}

形参变化不影响实参

#include <iosstream.h>
void swap(float *m, float *n) // 这里的*是定义指针用的
{
	float *temp; //指针变量
	temp = m; // m指针变量本身,而不是它所指的内容
	m = n;
	n = temp; // 交换了m指针和n指针,更改了m和n所指向的地址
	// 执行完形参mn就被释放了,没了
}

void main()
{
	float a,b,*p1,*p2;
	cin>>a>>b; //从键盘输入值赋给a和b
	p1=&a; // p1指针指向a的地址
	p2=&b; //p2指针指向b的地址
	swap(p1,p2); 
	cout<<a<<endl<<b<<endl; // ab本身没有改变
}

传地址方式——数组名作参数

  • 传递的是数组的首地址
  • 对形参数组所做的任何改变都将反应到实参数组中
#include <iosstream.h>
	void sub(char b[]){ //b[]括号里不能指定大小,传递的是首地址。也可以写成*b
	b[] ="world"; // 在该数组地址上重新赋值
	}

void main(void){
	char a[10] ="hello";
	sub(a); // sub是子函数,将数组名a传递过去,其实就是传递了首地址
	cout<<a<<endl;
	
}

传地址方式——引用类型作参数

引用:它用来给对象提供一个替代的名字。

  1. 传递引用给函数与传递指针的效果是一样的,形参变化实参也发生变化
  2. 引用类型做形参,在内存中并没有产生实参的副本,它直接对实参操作;而一般变量做参数,形参与实参就占用不同的存储单元,所以形参变量的值是实参变量的副本。因此,当参数传递的数据量较大时,用引用比用一般变量传递参数的时间和空间效率都好。
  3. 指针参数虽然也能达到与使用引用的效果,但在被调函数中需要重复使用*指针变量名的形式进行运算,这很容易产生错误,且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。
#include <iosstream.h>
void main(){
	int i=5;
	int &j=i; //j是一个引用类型,代表i的一个替代名
	//i值改变时,j值也跟着改变
	i=7;
	cout<<"i = "<<i<<"j = "<<j; //所以i=7 j=7
	
}
#include <iosstream.h>
void swap(float &m, float &n) // 引用类型
{
	float temp; 
	temp = m; 
	m = n;
	n = temp; // mn的值改变,因为mn等同于ab的替代名,内容都改变了
	// 执行完形参mn就被释放了,没了
}

void main()
{
	float a,b;
	cin>>a>>b; //从键盘输入值赋给a和b
	swap(a,b); 
	cout<<a<<endl<<b<<endl; // ab也改变了
}

顺序表基本操作的实现

操作算法中用到的预定义常量和类型

// 函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2

// status 是函数的类型,其值是函数结果状态代码
typedef int Status;
typedef char ElemType;

线性表L的初始化

这里传值方式用的是引用

Status InitList_Sq(SqList &L){ // 构造一个空的顺序表L
	L.elem =new ElemType[MAXSIZE]; // 为顺序表分配空间
	if(!L.elem)	exit(OVERFLOW);  // 存储分配失败
	L.length=0; // 空表为0
	return OK;

}

销毁线性表L

void DestroyList(SqList &L){
	if(L.elem)delete L.elem; //释放存储空间
}

清空线性表

void ClearList(SqList &L){
	L.length=0; //将线性表的长度置为0
}

求线性表L的长度

int GetLength(SqList L){
	return(L.length); 
}

判断线性表L是否为空

int IsEmpty(SqList L){
	if(L.length==0) return 1;
	else return 0;
}

顺序表的取值

根据位置i获取相应位置数据元素的内容

int GetElem(SqList L,int i,ElemType &e){
	if(i<1||i>L.length) return ERROR; // 判断i值是否合理,若不合理,返回ERROR
	
	e=L.length[i-1]; //第i-1的单元存储着第i个数据
	return OK;
}

顺序表是随机存取,时间复杂度为常量级,$O(1)$