本章主要介绍课程的框架、会使用到的C,C++的基本语法以及空间、时间复杂度的计算方法。
全课程总共分为:线性表、栈与队列、字符串、矩阵、树与二叉树、图、排序和查找这些内容。
线性表为最基础内容,包括:顺序表的建立和相应的增删改查操作;链表的种类:单链表、双链表、循环单链表、循环双链表、链表的两种基本建立方法和相应的增删改查操作等等。
栈与队列:分为线性和链式两种,掌握它们实现栈和队列的方法。掌握栈的FILO和队列的FIFO的性质,会利用它们的性质完成典型的任务,例如字符串匹配。
字符串:了解字符串基本知识,KMP匹配算法
矩阵:数组,稀疏矩阵压缩
树与二叉树:树的存储方式、建立、节点与叶子数目的关系、性质、遍历方式、与森林的转化、二叉树的性质、建立、遍历方式、平衡二叉树的建立与插入删除、哈夫曼树与哈夫曼编码
图:图的存储方式、种类、变量算法、最小生成树、最短路径、关键路径、拓扑排序
排序:时间复杂度O(n)、稳定性、排序过程、交换类排序、选择类排序、插入类排序、归并排序、基数排序(桶排序)、外部排序
查找:顺序查找、二分查找、二叉树存储查找、散列查找、散列表(Hash Table)的建立以及解决冲突的几种方法
相信能够修数据结构的童鞋有一定的编程基础,这里就不说那些最基础的概念了,否则文档讲的就不是数据结构了。这里我们重点介绍一些数据结构常用的基础知识。
基本类型:float、int、char等均属于此类
结构型:用户自己设计的数据类型,如利用struct实现。举个例子:
typedef struct Student {
int ID;
string name;
int age;
} Student;
这里面我们定义了一个新的类型叫做Student,这个类型里面包括一个int类型的ID、一个string类型的name和一个int类型的age。
指针型:是地址,指向对应类型的某一个变量的地址。举个例子:
int *p;
int number = 233333;
p = &number;//将number的地址赋值给p
这里面int指针类型的变量p,它指向了一个int类型变量number,就是说此时p所存放的就是number的地址。
相信很多童鞋都对C/C++中的指针非常讨厌,那么在这里我们简单介绍一下有关指针的基本操作,目的还是为了方便后面的内容。再举个例子:
#include
using namespace std;
int main() {
int a = 233333;
int b;
int *p
//将a的地址赋值给p,此时p的内容就是a的地址了,&是取地址的意思
p = &a;
//获取p指向的地址存放的内容,*是根据地址获取内容,此时b的值就是p指向的地址中的内容
b = *p;
cout << "a = " << a << endl;
cout << "&a = " << &a << endl;
cout << "p = " << p << endl;
cout << "*p = " << *p << endl;
cout << "b = " << b << endl;
return 0;
}
输出结果:
a = 233333
&a = 0x29ff14
p = 0x29ff14
*p = 233333
b = 233333
我们之后会经常用到链表这种结构。我们都知道,链表是由很多个节点连接起来的,那么每个节点是如何定义的,我们在这里简单说明一下。由于没有提供直接使用的节点类型,我们通常都使用上文提到的struct来自己声明。举两个例子:
//单链表的一个节点包括:这个节点所存储的数据vlaue,以及指向下一个节点地址的指针。
//由于下一个节点仍然是我们定义的Node类型,所以就要声明一个Node类型的指针来指向。
typedef struct Node {
int value;
struct Node* next;
} Node;
//二叉树的一个节点包括:存放的数值value、指向左子树节点的指针、指向右子树节点的指针。
//由于子树节点仍然是我们定义的BTNode类型,所以就要声明两个BTNode类型的指针来指向。
typedef struct BTNode {
int value;
struct BTNode* lchild;
struct BTNode* rchild;
}
函数是封装起来,用来执行相应功能的代码模块,为了避免代码的冗余和维护的方便,我们通常都把具有相同功能或者具有目的性要求的代码写成函数来调用。
在C语言这种面向过程的设计中,有一种很重要的思维方式叫做top-down design。也就是我们熟知的分治法。拿到一项任务,我们需要分析,要完成此任务大致需要做那些事情,分成一系列函数去做。接着继续分析,为了完成上述函数的功能,我们又应该怎样去做,再分成一系列更简单但是更为抽象的函数,以此类推,直到最底层可以用代码直接去实现简单的函数。这种做法的好处是设计时候的思路清晰,并且容易debug和维护。
这一点非常像我们在社团里面去写一个活动的策划,为了完成一个活动,我们需要做哪些大方向的工作,为了做这些大方向的任务,又要把每个大的任务分解成若干小任务。举个例子,要设计一个学生管理系统,我们应该按照这种思维方式去做:
说到函数,我们就要提到参数了。函数是用来实现某些功能的,重要的功能之一就是对传进函数的数据做出改变。call by value与call by reference是C/C++中函数调用经常出现的话题。
所谓的call by value,我们给函数传进去参数之后,可以理解为在函数里面有了一个参数的copy,我们所做的一切事情都是对copy来做,函数结束,对原来传进来的参数毫无影响。
call by reference,我们在函数中做的修改直接就是对参数本身。call by reference我们是通过传进来参数的地址来实现,通过改变地址指向的内容,就会修改参数本身的值啦。举个非常经典的例子:
我们需要设计一个函数来交换两个数的值:
#include
using namespace std;
void Exchange1(int A, int B);
int main() {
int a = 23333;
int b = 66666;
Exchange(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
/*
* @fun Exchange A and B using call by value.
* @param A The first parameter
* @param B The second parameter
*/
void Exchange1(int A, int B) {
int temporary;
temporary = A;
A = B;
B = temporary;
}
通过结果,我们可以知道,这个函数并没有起到作用,这是因为call by value,在函数里面交换的只是参数的一个copy而已。相比之下,我们看一下call by reference:
#include
using namespace std;
void Exchange2(int *A, int *B);
void Exchange3(int &A, int &B);
int main() {
int a = 23333;
int b = 66666;
Exchange2(&a, &b);
//Exchange3(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
/*
* @fun Exchange A and B using call by value.
* @param A The first parameter
* @param B The second parameter
*/
void Exchange2(int *A, int *B) {
int temporary;
temporary = *A;
*A = *B;
*B = temporary;
}
/*
* @fun Exchange A and B using call by reference.
* @param &A The first parameter's address
* @param &B The second parameter's address
*/
void Exchange3(int &A, int &B) {
int temporary;
temporary = A;
A = B;
B = temporary;
}
通过实验结果,我们可以看到利用Exchange2与Exchange3这两个函数都可以保证两个数的值交换。这两种写法都是引用类型,call by reference,函数交换的就是这两个参数本身。
小结:我们在之后的学习中会遇到很多参数的例子或者需要自己动手实现,我们需要注意的是:如果需要修改参数本身,则一定要传进引用类型,如:
//非引用类型,x值不变
void fun1(int x) {
++x;
}
//引用类型,x值加1
void fun2(int &x) {
++x;
}
当参数是数组的时候不必加入&,因为数组本身就是一种特殊的指针,会直接修改其内容。如:
//引用一维数组
void fun3(int x[], int n) {
……
}
//引用二维数组,第二维要注明大小
void fun4(int x[][50], int n) {
……
}
在涉及链表操作的时候,包括链式的树等等,我们通常需要插入新的节点。由于新的节点占据内存空间,我们需要手动为新的节点开辟空间。具体语法如下:
//方法一:
BTNode bt;
//方法二,通用:
BTNode *bt;
bt = (BTNode*)malloc(sizeof(BTNode));//开辟一块空间,bt指针指向该空间
学过C的童鞋非常熟悉i++和++i,这是前缀表达式与后缀表达式,这里涉及到计算先后的问题.
大部分的时候:
++i <=> i = i + 1 <=> i += 1;
i++ <=> i = i + 1 <=> i += 1;
–i <=> i = i - 1 <=> i -= 1;
i– <=> i = i - 1 <=> i -= 1;
然而在有些时候它们带来的结果是不同的。当表达式很长的时候,前缀表达式最先计算,后缀表达式最后计算。
还是举一个例子:
#include
using namespace std;
int main() {
int x = 100;
int y = 100;
int z = x++;
int w = ++y;
cout << "x = " << x << endl;
cout << "y = " << y << endl;
cout << "z = " << z << endl;
cout << "w = " << w << endl;
return 0;
}
输出结果:
x = 101
y = 101
z = 100
w = 101
分析:
执行了x++,相当于x = x + 1,x变为101
执行了++y,相当于y = y + 1,y变为101
表达式先执行了z = x,再执行x = x + 1,z仍为100
表达式先执行了y = y + 1,再执行w = y + 1,z为101
数据:客观事物的符号表示,在计算机中指所有能输入到计算机并被计算机程序处理的符号的总称。
数据元素:数据的基本单位
数据项:最小单位,一个数据元素由若干数据项组成
数据对象:性质相同的数据元素的集合,是数据的一个子集。
我们考虑数据元素是图书馆的一本书,数据项可以是书名、书的ID、书的作者……,数据对象可以考虑为所有C++的书籍。
逻辑结构:包括:集合、线性结构、树形结构、图状结构
存储结构(物理结构):包括:顺序结构(存放地址是连续的)、链式结构(存放地址是否连续没有要求)
时间复杂度:算法中基本操作的执行次数。首先需要明确算法中哪些是基本操作,然后计算出基本操作所重复执行的次数。我们找到一个n,称为问题规模。并且基本操作重复执行的次数是n的一个函数f(n),求出f(n)之后取f(n)中随n增大而增长最快的项,将其系数变为1,作为时间复杂度的度量。记为T(n) = O(f(n))。我们一般将最坏情况作为算法复杂度的度量。举个例子:
f(n)与n无关时,T(n) = O(1)
f(n)与n线性相关时,T(n) = O(n)
时间复杂度排序:
空间复杂度:算法运行时所需存储空间的度量,在之后的文档中根据需要介绍。
到这里为止,我们已经讲了整门课程的知识框架、各章节的关系、数据结构的基本概念、算法的空间和时间复杂度计算以及需要具备的基础C/C++知识,接下来的章节,我们会按照框架图进行介绍。介绍的形式包括文字、图片以及笔者使用C++写的样例代码。数据结构本质上还是和代码打交道,所以希望大家在了解理论知识的同时,多去动手写代码,尤其是讲解树、图、排序等涉及算法过程的知识点。