《数据结构与算法》诸论笔记(最全)

本文围绕数据结构与算法展开,介绍了数据结构的研究对象、基本概念和术语,包括数据、逻辑结构等。阐述了算法的概念和分析方法,如正确性、高效性等。还讲解了学习算法与数据结构的意义和方法,并介绍了C语言的数据类型、数组、结构体和指针类型及其应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


《数据结构与算法》

数据结构的基本概念与学习方法
算法与数据结构
学习算法与数据结构的意义和方法

1.1 数据结构的基本概念与学习方法

1.1.1 数据结构的研究对象

​ 数据结构作为一门学科,主要研究数据的各种罗技结构和存储结构以及对数据的各种操作。它主要有3方面的内容:数据的逻辑结构、数据的物理存储结构、对数据的操作(算法、运算)。通常,算法的设计取决于数据的逻辑结构,算法的实现取决于数据的_物理存储结构_。

1.1.2 数据结构的基本概念和基本术语

1. 数据

​ 数据(Data)是所有能被输入到计算机中,且被计算机处理的符号的集合。计算机的一些功能特性本身不是数据,只能通过编码转换成被计算机识别、存储和处理的符号形式后才是数据。

2. 数据元素

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

3. 数据项

​ 数据元素是数据结构中讨论的最小单位。若数据元素可再分,则每个独立的处理单元就是数据项,数据元素是数据项的集合;若数据元素不可再分,则数据元素和数据项是同一个概念。

4. 数据结构

​ 数据结构是相互之间存在一种或多重特定关系的数据元素的集合。这个概念涉及了两个内容:一个是数据元素;另一个是数据元素之间的相互关系。数据元素不是孤立存在的,在它们之间总是存在某种相互关系。

5. 逻辑结构

​ 逻辑结构是数据元素之间的相互逻辑关系, 它与数据的存储无关,是独立于计算机的。
根据数据元素之间的关系的不同特性,有下列4类基本逻辑结构。

  1. 线性结构:数据结构中的元素之间存在一对一的相互关系。
  2. 树形结构:数据结构中的元素之间存在一对多的相互关系。
  3. 图形结构:数据结构中的元素之间存在多对多的相互关系。
  4. 集合结构:数据结构中的元素之间存在除了“同属一个集合”的相互关系之外,别无其他关系。

~~ 我觉得数据结构和逻辑结构最大的区别就在于,数据结构是宏观的面向的是数据元素而逻辑结构相对于偏微观一些是面向数据项的,但两者是无可或缺的 ~~

6. 物理结构

​ 物理结构(存储结构)是数据结构在计算机中的表示(又称映像),它包括数据元素的/ *1机内表示/和关系的机内表示。由于具体实现的方法有顺序、链接、索引、/ *2散列/等多种,所以,一种数据结构可表示成一种或多重存储结构。
数据元素的映像方法 | 关系的映像方法

7. 数据类型

​ 数据类型是与数据结构亲密相关的一个概念。数据是按数据结构分类的,具有相同数据结构属同一类。同一类_数据的全体_称为一个数据类型。数据类型又被认为是一个值的集合和定义在这个值集合上到一组操作的总称。
​ 按值是否可分解,高级语言中的数据类型可以分为两类:原子类型和结构类型,原子类型的值是不可分解的。结构类型的值是由于成分按某种结构组成的,因而是可以分解,并且它的成分既可以是非结构化的,也可以是结构化的,也可以是结构化的。

8. 抽象数据类型

​ 抽象数据结构(ADT)是指一个数学模型以及定义在此数学模型上到一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
​ 抽象数据类型和数据类型实质上是一个概念,抽象的意义在于数据类型的数学抽象特性。抽象数据类型是描述数据结构的一种理论工具,其目的是使人们能够独立于程序的实现细节来理解数据结构的特性。一种数据结构被视为一个抽象数据类型,即数据结构的数据视为抽象数据类型的数据对象,数据结构的关系视为抽象数据类型的数据关系,数据结构上的算法视为抽象数据类型的基本操作。抽象数据类型的特征是将使用与实现相分离,从而实现封装和信息隐藏。抽象数据类型通过一种特定的数据结构在程序的某个部分得以实现,而在设计使用抽象数据类型的程序时,只关心这个数据类型上的操作,而不关心数据结构的具体实现。

~~ *这个我觉得其实就是一个思考解决问题先后顺序的问题我们要解决问题,先是在梳理逻辑上进行解决,数理逻辑上这就是抽象的,然后接下来用具体的工具就是我经常说的二进制思维,来从工程上实现, 这个就根据具体情况所使用的工具不同从而有不同的类型方法来解决 *

1.2 算法与数据结构

算法 + 数据结构 = 程序

1.2.1 算法概念

​ 算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指表示一个或多个操作。
1.有穷性
2.确定性
3.可行性
4.输入
5.输出
​ 算法的含义与程序十分相似,但二者是有区别的。一个程序不一定满足有穷性。操作系统,只要整个被破坏,它就永远不会停止,即使没有任务要处理,它仍处于一个等待循环中,以等待新任务的进入。因此,__ 存储系统 __不是一个算法。此外,程序中的指令必须是机器可执行的,而算法中的指令则无此限制。

~~ 从中可以看出,一个问题的答案只有一个但是可以用无数多种方法进行解答在计算机领域来说就是算法了,设计者设计算法的时候要选择合适的逻辑结构和物理结果,进而设计出比较满意高效的算法。~~

1.2.2 算法分析

1. 正确性(correctness):

​ 算法应能正确地实现预定的功能(即处理要求)。对算法是否正确的理解有以下4个层次。

1.程序中不含语法错误;
2.程序对于__ 几组 __输入数据能够得出满意要求的结果;
3.程序对应精心选择的、典型的、苛刻且带有刁难性的几组输入数据能够得出满意要求的结果;
4.程序对于一切合法的输入数据都能得出满足要求的结果。
通常以第3层意义的正确性作为衡量一个算法是否合格的标准。

2. 可读性(readability):

​ 算法应易于阅读和理解,以便于调试、修改和扩充。

3. 健壮性(robustness):

​ 当输入数据非法时,算法也能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。

4. 高效性(efficient):

​ 即要求执行算法的时间短,所需要的存储空间少。

~~ 在linux terminal下可以使用此命令查看代码的存储空间,"sudo du -sh ~/file/ ~~
虽然在应用中总是希望选用的一个占用存储空间小、运行时间短、其他性能也好的算法,然而实际上却很难做到十全十美,因为要求上通常会有相互抵触的现象。要节约算法时间往往要以牺牲更多的空间为代价;而为了节省空间又可能要以更多的时间为代价。因此只能根据具体情况有所侧重。

​ 运算时间是指一个算法在计算机上运算所花费的时间,它与简单操作(如赋值操作、转向操作、比较操作等)的次数有关。算法有控制结构和简单操作组成,算法的执行时间为简单操作的执行次数与简单操作的执行时间的乘积,而简单操作的执行时间__ 是由计算机硬件环境决定的,与算法无关__。

​ 问题的规模是算法求解问题的输入量,一般用整数n表示。

​ 算法的时间复杂度可看成是问题规模的函数,记为T(n)。
算法1.1 累加求和。

int sum(int a[], int n)
{
   int s = 0, i;                               //(1)给累加变量s赋除值
   for (i = 0; i < n; i++)                 //(2)进行累加求和
       s += a[i];
   return(s);                               //(3)返回s的值
}(1)(2)步不是简单操作,可将算法改写成为:

int sum(int a[], int n)
{
   int s, i;
   s = 0;                               //1次
   i = 0;                               //1次
   while (i < n)                    //n+1次
   {
         s += a[i];                 //n次
         i ++;                       //n次
    }
    return s;                     //1次
}

因此,算法1.1的时间复杂度为:T(n) = 3n + 4。
算法 1.2 矩阵想加。

//a, b, c分别为n阶矩阵,a,b表示两个加数,c表示和

void matrixadd(int a[][n], int b[][n], int c[][n])int i, j;
     for(i = 0; i < n; i++)
         for(j = 0; j < n; j++)
             c[i][j] = a[i][j] + a[i][j];
 }  

​ 通过与算法1.1相似的分析过程,可得到算法1.2的时间复杂度为: T ( n ) = 4 n 2 + 5 n + 2 T(n) = 4n^2 + 5n + 2 T(n)=4n2+5n+2

​ 算法1.1和算法1.2 的时间复杂度比较容易计算,因为算法比较简单,同时for循环中的循环次数是固定的。但是当算法比较复杂,同时包含有while等循环时,其时间复杂度的计算就相当困难了。实际上,一般没必要精确地计算出算法都时间复杂度,只要大致计算出相应都数量级(order)即可。

​ 设T(n)的一个辅助函数为 f ( n ) f(n) f(n),随着问题规模n的增长,算法执行时间的增长率和 f ( n ) f(n) f(n)的增长率相同,则可记作:
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
​ 当问题的规模n趋于无穷大时,把时间复杂度 T ( n ) T(n) T(n)的数量级(阶) O ( f ( n ) ) O(f(n)) O(f(n))成为算法的(渐进)时间复杂度。

​ 估算算法的时间复杂度常用方法如下。

  1. 多数情况下,求最深层循环内的简单语句(源操作)的重复循环执行的次数。
  2. 当难以精确计算源操作的执行次数时,只需求出它关于n的增长率或阶即可。
  3. 当循环次数未知(与输入数据有关),求最坏情况下的简单语句(源操作)的重复执行次数。

​ 估算算法的时间复杂度。

    程序段1:

    x = x + 1;

    程序段2for(i = 1; i <= n; i++)
        x++;

    程序段3for(i = 0; i <= n; i++)
        for(j = 1; j <=n; j++)
            x++;

    程序段4for(i = 1; i<= n; i++)
        for(j = i; j <=n; i++)
            x++;
	//语句x++的执行次数(或频数)分别为1、n、n^2和n(n+1)/2,则这4个程序段的渐进时间复杂度分别为O(1)、O(n)、O(n^2)和O(n^2)
    程序段5//将a中整数序列重新排列成自小至大有序的整数序列
        void bubble_sort(int a[], int n)
    	{
        	int temp, i, j;
        	int change = 1;
        	for(i = n -1; change = 1; i > 0 && change; i--)
            {
                change = 0;
                for(j = 0; j < i; j++)
                    if(a[j] > a[j+1])
                    {
                        temp = a[j];
                        a[j] = a[j+1];
                        a[j+1] = temp;
                        change = 1;
                    }
            }
    	}

​ 上述程序段中,基本操作作为赋值操作,由于次数是未知的,因此考虑最坏情况下的次数为 n ( n + 1 ) / 2 n(n+1)/2 n(n+1)/2,渐进时间复杂度 O ( n 2 ) O(n^2) O(n2)

​ 多项式时间算法的关系为:
O ( 1 ) < O ( l b n ) < O ( n ) < O ( n l b n ) < O ( n 2 ) < O ( n 3 ) O(1)<O(lbn)<O(n)<O(nlbn)<O(n^2)<O(n^3) O(1)<O(lbn)<O(n)<O(nlbn)<O(n2)<O(n3)

​ 指数时间算法的关系为:
O ( 2 n ) < O ( n ! ) < O ( n n ) O(2^n)<O(n!)<O(n^n) O(2n)<O(n!)<O(nn)
​ 当n值很大时,指数时间算法和多项式时间算法在所需时间上相差非常悬殊。

算法的空间复杂度为:
S ( n ) = O ( g ( n ) ) S(n)=O(g(n)) S(n)=O(g(n))
它表示随着问题规模n的增大,算法运行所需存储量的增长率与*g(n)*的增长率相同。

1.3 学习算法与数据结构的意义和方法

​ “算法与数据结构”这门课程不仅具有很强的理论性,而且有很强的实践性。因此,学习本门课程既要弄清出主要的数据结构的表示及操作实现的方法,又要认真地通过上机进一步实践,明白算法背后的逻辑架构和原理。

1.4 C语言的数据类型及其算法描述

1.4.1 C语言的基本数据类型概述

​ C语言的数据类型如下:

在这里插入图片描述

​ 有时需要自定义数据类型,其格式如下:

​ typedef 类型名 标识符

其中,类型名为已定义类型名,标识符为新类型名。

​ 例如:

​ typedef int elemtype; //把elemtype数据类型定义为整型类型

1.4.2 C语言的数组和结构体数据类型

1. 数组

  1. 数组类型特点:数据元素类型相同。

  2. 数组定义格式:元素数据类型名 数组名[常量表达式];

  3. 数组存储结构:顺序存储(数组名代表首地址)。

    例如:

    int a[4];

    数组a的存储结构如图1.8所示

在这里插入图片描述

​ 由数组的顺序存储,可得出式(1.3),从而实现数组元素的随机访问。
L o c ( a i ) = l o c ( a 0 ) + i ∗ L                                ( 1.3 ) Loc(a_i)=loc(a_0)+i*L\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;(1.3) Loc(ai)=loc(a0)+iL(1.3)
其中 L o c ( a i ) Loc(a_i) Loc(ai)表示元素 a i a_i ai的地址, l o c ( a 0 ) loc(a_0) loc(a0)表示数组的首地址, i i i表示下标, L L L表示一个元素所占字节数。

  1. 数组元素的引用。

​ 格式:

​ 数组名[下标]

其中,小标是取值为(0,n-1)的表达式, n n n为数组长度。

​ 关于数组元素的引用需注意以下几点。

  • ​ 数组元素按定义数组时定义的数组元素类型进行操作,它可出现在C语言表达式中的任何地方。
float a[4];					//定义一个有4个浮点型原元素的数组
a[3] = 13.5;				//对a[3]按浮点型数字操作,赋值为13.5

  • ​ C语言规定只能逐个引用数组元素而不能一次引用整个数组。

例如:

float a[4], b[4];			//定义两个有4个浮点型元素的数组
a = b;						//语法错误,其意图是想把b数组的所有元素赋值到a数组中,但C语言把数组名处理为连续存储单元的首地址(常量),常量不能出现在赋值运算符的左边
  1. 字符串。
    多个字符构成的序列称为字符串。字符串分为字符串常量和字符串变量。
    字符串常量是由双括号括起来的多个字符。
    C语言把存放字符串的变量表示为字符数组或指向字符指针。例如:
char s[7];				//定义一个最多7个字符的字符串数组
char *s;				//定义一个可指向任意长度字符串的字符指针变量

字符串是特殊的数组,既可对每个数组元素进行逐个操作,也可用字符串处理函数对字符串整体进行操作。
​ 字符串的处理。

void main()
{
	char s1[5], s2[5];
    scanf("%s%s", s1, s2);			//用控制符s表示对字符串输入,参数用数组名
    printf("%s\n%s\n", s1, s2);
}
input:

ABC	DEF
————————————————
output:

ABC
DEF

2. 结构体数据类型

  1. 结构体数据类型特点:数据元素类型不同,它们常作为一个整体出现。

  2. 结构体的说明:要定义一个结构体类型的变量,一般先定义结构体类型名,再定义结构体类型的变量名。

    ​ 结构体类型说明格式为:

    ​ struct 结构体名 {成员列表};

    struct student 
    {
        int num;
        char name[10];
        int age;
        float score;
    };
    

    ​ 上述定义了一个由4个成员所构成的一个结构体类型student,但此时没有变量定义,即没有分配存储单元,不能在程序中直接访问结构体类型名。

    ​ 结构体变量的说明如下。

    ​ 形式1:

    ​ struct 结构名 变量名;

    ​ 例如:

    ​ struct student s1, s2; //定义两个结构体类型为student的结构体变量

    ​ 形式2:

    ​ struct 结构体名{成员列表}变量名;

    ​ 例如:

    struct student
    {
    	int num;
    	char name[10];
    	int age;
    	float score;
    }s1, s2;				//定义两个结构体类型为student的结构体变量
    

    ​ 形式3:

    ​ struct {成员列表}变量名;

    ​ 例如:

    struct
    {
    	int num;
    	char name[10];
    	int age;
    	float score;
    }s1, s2;				//定义两个由4个成员所构成的结构体变量
    
    1. 结构体变量中成员的引用。

      格式:

      结构体变量名.成员名;

      例如:

      struct {int x; float y;}a;
      a.x = 3;a.y = 3.4;
      

      结构体变量a的存储结构如图1.9所示。

      对结构体变量中成员的操作,按定义结构体类型时该成员的类型进行操作,它可出现在C语言表达式中任何地方。
      在这里插入图片描述

3. 结构体数组

  1. 结构体数组特点:数组中的每个元素为结构体类型。

  2. 结构体数组定义如下。

    struct 结构体类型名 数组名[长度];

  3. 结构体数组的引用。

    结构体数组元素的引用格斯:

    数组名[下标表达式]

    结构体数组元素成员的引用格式:

    数组名[下标表达式].成员名

​ 程序段如下,其存储结构如图1.10所示。

在这里插入图片描述

struct student
{
	int num;
	int age;
	float score;
}s[5];				//定义一个由5个结构体元素组成的数组s
s[3].age = 20;		//对数组s的3号元素(结构体类型)不能整体操作
					//现对它的age成员(整体)进行赋值操作
s[3].score = 78.5;	//对3号元素的score成员(浮点型)进行赋值操作

1.4.3 C语言的指针类型概述

1. 指针类型特点

其值是某一存储单元地址的变量。

2. 指针类型变量的定义

指针类型变量的定义的格式:

类型标识符 * 变量名;

其中,类型标识符是该指针变量所指向的存储单元(指针变量的对象)的数据类型。

​ 例如:

​ int *p; //定义一个指向整型存储单元的指针变量

​ 说明:要访问指针变量所指向的存储单元,必须先对指针变量赋值。

​ 例如:

​ int *p, a; //定义一个指向整型存储单元的指针变量p和一个整型变量a

​ p = &a; //对p赋值为a的地址,即p的值目前为a变量的地址

3. 与指针相关的计算

  1. &:取操作符对象(内存单元)的内存地址。
  2. *:取指针所指向的存储单元的内容,即间接引用存储单元(指针对象)的内容。

例如:

int *p; //定义整型指针变量p

int i;

p = &i; //指针变量p赋值

*p = 3; //对p所指向的存储单元赋值,此时相当于将变量i赋值为3

[!CAUTION]

“int *p;”语句中 为定义指针 变量的标志,而“ *p = 3;”语句中 * 为间接引用运算符,相当于用指针变量对变量i赋值。

​ 指针&与*运算举例。

程序段如下,其示意图如图1.11所示。

int i, *P, I = 3, J;			//内存分配如图1.11(a)所示
P = &I;							//执行结果如图1.11(b)所示
J = *P;							//执行结果如图1.11(c)所示

在这里插入图片描述

4. 指针的使用和运算

  1. 赋值运算

​ 对像类型相同的指针变量之间可以相互赋值,表示指向同一对象。

​ 例如:

int *p1, *p2, a;
p1 = &a;				//指针赋值运算
p2 = p1;				//此时,a既是p1的对象,又是p2的对象

其执行结果如图1.12(a)所示。

  1. 算术运算

​ 例如:

​ p++; //p指向下一对像单元,p的值不是增加1,而是增加对象类型占的字节数

​ 例如:

​ int A[4], *p;

​ p = A;

​ p++;

​ 其执行结果如图1.12(b)所示。

在这里插入图片描述

[!WARNING]

数组名与指针都表示地址,但数组名为地址常量,指针为变量,指针可以指向不同的地址(对象)。

  1. 与指针有关的库函数

与指针有关的库函数如表1.2所示。

在这里插入图片描述

5. 指向数组元素的指针变量

​ 当数组元素的类型和指针变量类型相同时,可以将数组名赋值给指针变量,使该指针变量指向数组元素,对数组元素就可以通过下标法或指针法来访问。

​ int a[10], *p;

​ p = &a[0]; //或p = a;这样p为指向数组元素的指针

​ 此时,p+i, a+i, &a[i]三者等价,都表示a[i]元素的地址。

​ 同时,*(p+i), *(a+i), *&a[i]和a[i]四这等价,都表示a[i]元素的值。

  1. 下标法
void main()
{
	int i, min, max, a[10];
	for (i = 0; i < 10; i++)
		scanf("%d", &a[i]);
	min = max = a[0];
		for(i = 1; i < 10;i++)
		{
			if(a[i] > max)max = a[i];
			if(a[i] < max)min = a[i];
		}
		printf("max = %d, min = %d\n", max, min);
}
  1. 指针法
void main()
{
	int *p, min, max, a[10];
	for(p = a; p < a + 10; p++)
		scanf("%d", p);
	p = a;
	min = maxx = *p;
	for(p = a + 1; p < a + 10; p++)
	{
		if(*p > max)max = *p;
        if(*p < min)min = *p;
	}
	printf("max = %d, min = %d\n", max, min);
}

6. 指向结构的指针

  1. 指向结构的指针的特点:指针对象为结构体类型。

  2. 指向结构的指针定义:

    struct 结构名 *变量名

  3. 引用成员。

​ (*指针变量名).成员名

等价于:

​ 指针变量名 -> 成员名

例如:

struct student
{
	int num;
	int age;
	float score;
}s, *p;
p = &s;
p -> num = 10;
p -> score = 85.5;

上述程序段的执行过程如图1.13所示。

在这里插入图片描述

为了总结指针、结构体类型的使用,现通过例1.15的程序段进行综合说明。

程序本身无实际意义,仅为了说明各数据类型的使用方法,为后续的速发描述打下基础。程序执行示意图如图1.14所示。

struct object
{
	int data;
	struct object *next;
};
#define NULL 0
#define LEN sizeof(struct object)
void main()
{
	struct object *p, *q, *ptr;
	//定义指针变量,分配指针变量的内存单元,但此时指针变量无确定值,即无对象。
	p = (struct object *)malloc(LEN);
	//动态产生一个新节点(结构体存储单元),并把其地址赋给指针变量p,此时p有对象
	//目前的对象是新产生的结点,如图1.14中(1)所示。
	p -> data = 5;			//对p的对象的data成员赋值,按int类型操作,如图1.14(2)
	q = (struct object *)malloc(LEN);	//如图1.14(3)所示
	q -> data = 10;			//如图1.14中(4)所示
	p -> next = q;			//对p的对象的next成员赋值为q,按指针类型操作,达到两个结点的链接
	//注意:此时是对p的next成员赋值,而p自己没有修改,如图1.14中(5)所示
	q -> next = NULL;
	//对q的对象的next成员赋值为空指针,表明链接的结束,如图1.14中(6)所示
	p -> next -> data = 20;
	//对p的后继(next)的数据(data)修改,此时即是对q的数据(data)修改,如图1.14中(7)所示
	ptr = p;			//ptr指向p的结点,如图1.14中(8)所示
	p = p -> next;
	//p指向p的后继(注:见第二章),指针后移,如图1.14中(9)所示,此时,原结点只能通过ptr来访问
	}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值