本文参考网课为 数据结构与算法 1 第一章概论,主讲人 张铭 、王腾蛟 、赵海燕 、宋国杰 、邹磊 、黄群。
本文使用IDE为 Clion,开发环境 C++14。
更新:2023 / 10 / 15
结构,即实体 + 关系。
数据结构,即按照逻辑关系组织起来的一批数据,按一定的存储方法被存储在计算机中,在这些数据上定义了一个运算的集合。示意图如下所示:

数据结构 的 逻辑 是指数据本身及其之间的关系。主要有2类:
线性结构
线性表表、栈、队列、串 等非线形结构
树二叉树、Huffman树、二叉检索树 等图有向图、无向图 等数据结构 的 存储 是 逻辑 结构到( 内存 的)物理存储单元的映射。
计算机主存储器,即 内存,是由非负整数进行低地址到高地址编码的线性结果,基本单位是 字节。我们想访问哪一个地址单元,就可以立即访问到且需要的时间基本相同,不需要在整个内存中搜索或者遍历。
数据结构 的 存储 结构主要有 4 类:
顺序链接索引散列数据结构 的 运算 是指数据操作,体现为函数。
抽象数据类型,被简称为 ADT( Abstract Data Type ),定义了一组运算的数学模型,与物理存储结构无关,使得软件系统建立在数据之上(面向对象)。
模块化的思想的发展的要求,使得运算实现的细节和内部数据结构被隐藏,便于软件复用。
栈 的抽象数据类型 ADT:

逻辑结构数据操作
push )、出栈( pop )、取栈顶( top ) 、判断栈是否为空( isEmpty )template // 栈的元素类型为T
class stack{
public: // 栈的运算集
void clear(); // 变为空栈
bool push(const T item) // item入栈,成功则返回真,否则为假
bool pop(T & item); // 弹栈顶,成功则返回真,否则为假
bool top(T & item); // 读栈顶但不弹出,成功则返回真,否则为假
bool isEmpty(; // 若栈已空 返回真
bool isFull(; // 若栈已满 返回真
};
目标是对 问题 求解。
问题( Problem )算法( Algorithm )问题 求解过程的描述,是指令的有限序列。程序( Program )算法 在计算机程序设计语言中的实现。通用性
有效性
确定性
以下面的例子为例,顺序在一个无序的数组里找K值,在索引为0的地方放置一个监视哨。

#include
#include
using namespace std;
template <class Type>
class Item{
private:
Type key; // 关键码域
// 其它域
public:
Item(Type value):key(value){}
Type getKey(){return key;} // 取关键码值
void setKey(Type k){key=k;} // 置关键码
};
vector<Item<Type>*> dataList;
template <class Type> int SeqSearch(vector<Item<Type>*>& dataList, int length, Type k){
int i=length;
dataList[0]->setKey(k); // 将第0个元素设为待检索值
while (dataList[i]->getKey()!=k) i--;
return i; // 返回元素位置
}
对于已排序的顺序线性表,

Kmid
Kmid = K,结束;Kmid > K,继续向前检索;Kmid < K,继续向后检索;#include
#include
using namespace std;
template <class Type> int BinSearch(vector<Item<Type>*>& dataList, int length, Type k) // 传入数组、数组长度以及待检索的k值
{
int low=1, high=length, mid;
while (low<=high){
mid=(low+high)/2;
if (k<dataList[mid] -> getKey())
high = mid-1; // 右区间边界左移,缩小搜索区间
else if (k>dataList[mid] -> getKey())
low = mid+1; // 左区间边界右移,缩小搜索区间
else return mid; // 成功返回k值的位置索引
}
return 0; // 检索k值失败,返回
}
算法的复杂性分析中最重要的表达式是 大O表达式,被用来表达函数增长率上限。
函数 f、g 定义域为自然数,值域为非负实数集。
如果存在正数 c 和算法规模 n0,使得对任意的 n>=n0,都有 f(n)<=cg(n),则称 f(n) 在集合 O(g(n)) 中。

f(n) = O(g(n)),当且仅当存在两个参数 c>0,n0>0,对于所有的 n>n0 都有 f(n) <= cg(n)。
当 n 足够大时,g(n) 是 f(n) 的上界。
如何衡量 大O表示法 呢?即看算法段内它的基本运算是什么。
大O表示法 有哪些重要的运算规则呢?
f1(n) + f2(n) = O(max(f1(n), f2(n)))f1(n) 和 f2(n),则实际上最耗时(复杂度最大)的那个会更重要。
if 结构,switch 结构f1(n)*f2(n)=O(f1(n)*f2(n))for 结构、while 结构、do-while 结构算法的复杂性分析的 大Ω表达式,被用来表达函数增长率的所有下限中那个最 “紧”(即最大)的下限。
函数 f、g 定义域为自然数,值域为非负实数集。
如果存在正数 c 和算法规模 n0,使得对任意的 n>=n0,都有 f(n)>=cg(n),则称 f(n) 在集合 Ω(g(n)) 中。
它和 大O表达式 的唯一区别在于不等式的方向。

当上、下限相同时则可用 大Θ表达式。
函数 f、g 定义域为自然数,值域为非负实数集。
如果一个函数既在集合 O(g(n)) 中又在集合 Ω(g(n)) 中,则称其为 Θ(g(n))。即,当上、下限相同时则可用 大Θ表达式。
存在正常数 c1、c2,以及正整数 n0,使得对于任意的正整数 n>n0,有下列两不等式同时成立:
c1g(n)<=f(n)<=c2g(n)

n 的一维数组中找出一个给定的 K 值。1 个元素就是 K,即只需要检查一个元素,复杂度为 O(1)K 是数组中最后一个元素。检查数组中的所有 n 个元素,才能发现 K 是最后一个元素,复杂度为 O(n)K 值出现在 n 个位置上的概率都是 1/n,则平均代价为 O(n),(1+2+…+n)/n=(n+1)/2`1/21/4(1-1/2-1/4)/(n-2)=1/4(n-2)O(n), 1+(n+3)/8K 值。K,即只需要检查一个元素,复杂度为 O(1)O(logn)数据结构 要求:
空间空间 来存储它的每一个数据项时间时间 来执行单个基本操作意味着我们需要对 时间 和 空间 进行权衡,也是对 代价 和 效益 的权衡:
空间 开销,可能改善算法的 时间 开销空间,往往需要增大运算 时间仔细分析所要解决的问题
特别是求解问题所设计的 数据类型 和数据间 逻辑 关系 —— 问题抽象、数据抽象
数据结构 的初步设计往往先于算法设计
注意 数据结构 的可扩展性
考虑当输入数据的规模发生改变时,数据结构 是否能够适应求解问题的演变和扩展
总结以上几点,可分为以下几步:
类 与 对象
类 的概念及基本语法C 语言是一门面向过程的程序设计语言。C++ 语言是一门面向对象的程序设计语言,对象 是 类 的实例。因此,有必要了解 类 的概念。this 指针流
举以下手机的例子来简要说明 类 与 对象 的概念:
A使用H品牌的手机,B使用A品牌的手机,C使用O品牌的手机。
类,因其具有相同的特点(或,属性 ),例如亮度、电池电量、运营商。对象。类 的 方法。我们可以对同一 类 中的 对象 使用相同的 方法 进行操作。方法 时需指定具体 对象,比如,C的手机。从手机这一 类 到具体某部手机 对象 的过程,称为 类 的实例化。共同特点( 变量 )构成数据结构
归纳行为( 函数 )操作数据结构(抽象)
当对 变量、函数 进行捆绑和封装,便形成了 类。
以定义一个 矩形 类 为例,

class Rectangle{
public: // 用户访问
int w, h; // 定义变量:长、宽
int Area(){ // 定义方法:计算面积
return w*h;
}
int Perimeter(){
return 2*(w+h); // 定义方法:计算周长
}
void Init(int w_, int h_){ // 设置长宽
w = w_;
h = h_;
}
};
int main(){ // 定义主函数
int w, h; // 定义长和宽
Rectangle r; // r是一个对象,是类的具体实例化
cin >> w >>h; // 读入长和宽的数据
r.Init(w,h); // 使用Init方法设置长和宽
cout << r.Area() << endl << r.Perimeter(); // 调用Area和Perimeter,并输出结果
return 0;
}
类 时,如果我们像定义变量一样定义的 类,例如Rectangle r1, r2;
我们用 对象名.成员名 来使用 类 的成员函数和成员变量:
r1.w=5; r2.Init(5,4);
类 是一个指针,例如Rectangle * p1=&r1, p2=&r2;
我们用 指针->成员名 来使用 类 的成员函数和成员变量:
p1->w=5; p2->Init(5,4);
Rectangle & rr = r2;
我们用 引用名.成员名 来使用 类 的成员函数和成员变量:
rr.w = 5; rr.Init(5,4);
声明和定义可以分离。
class Rectangle{
public:
int w,h;
int Area(); // 声明方法:Area
int Perimeter(); // 声明方法:Preimeter
void Init(int w_h, int h_); // 声明变量:长、宽
};
int Rectangle::Area() {return w*h;} //::表示这是Rectangle类的成员函数Area
int Rectangle::Perimeter() {return 2*(w+h);}
void Rectangle::Init(int w_, int h_){w=w_; h=h_;}
对于成员函数中的成员的访问权限,
| 成员 | 权限 |
|---|---|
private | 只能在成员函数中访问 |
public | 可以在任何地方访问 |
protected | 此处暂不介绍 |
class className{
private: // 私有属性和函数,在类的成员函数内部能够访问
public: // 公有属性和函数,当前对象的全部属性、函数
protected: // 保护属性和函数,同类其他对象的全部属性、函数
};
当我们声明1个类时,就会有一些成员函数自动生成。我们只需要定义一个空的类,它们就会存在。
构造函数 与 析构函数class cellphone{
public:
cellphone();
~cellphone();
};
cellphone(const cellphone&);
cellphone & operator=(const cellphone&);
cellphone * operator&();
const cellphone* operator&() const;
this 指针并非对象的成员函数,是一个常量指针。
每个对象可以使用 this 指针访问自己的地址。
在成员函数调用时,this 指针为隐式参数。
它的功能有防止自赋值、返回以连续调用。
下面是一个 this 指针的例子,
比如说,我们定义一个类叫做 Complex,
class Complex{
float real, image; // 默认是private成员,不可以被访问的
public:
Complex * ReturnAddress(){ // 定义成员函数ReturnAddress(), 其返回的是Complex类的指针类型
return this;
} // c.ReturnAddress() 等效于 &c
float ReturnReal(){
return this -> real; // 等效于 return real;通过this指针访问其中的成员变量/函数
}
};
函数模板 提供一种方法,让我们能够一次性定义一系列函数。
比如说,如果我们需要定义一个排序函数,如果对于整型定义一个排序函数、对于浮点数型定义一个排序函数、对于每一个不同类型的数据都定义一个排序函数,这是非常麻烦的。因此,我们可以使用类似于下方的函数模板定义对不同类型数据均可用的排序函数 sort。
template<class T> // 以template开头表示这是一个模板函数,后面跟着的class T表示这个函数模板的参数
return-type sort(...T...) // 返回值后面跟函数名sort,sort后的()填参数列表T
再比如,我们可以定义一个实际的输出函数:
template<class T>
void print(const T array[], int size){
int i;
for (i=0; i<size; i++) cout<<array[i];
return;
}
然后使用 int a[10]; print(a,10); 来调用函数。
由于 a 是 int 类型,编译器自动会把 T 替换成 int 来执行这个过程。
一个函数模板可以有多个待替换的参数。像下面的例子一样,它现在有2个参数,除此之外我们也可以在形参列表里来定义其它我们需要的变量,之后进行输出,
template<class T1, class T2>
void print(T1 arg1, T2 arg2, string s, int k)
{cout<<arg1<<s<<arg2<<k<<endl; return;}
类模板 和 函数模板 的功能差不多。
为了多快好省地定义出一批相似的类,可以定义 类模板,然后由 类模板 生成不同的类。
在 类 中我们经常会实现一种数据结构,看起来像一个容器,比如说 数组。它可以容纳很多的数据,数据的种类可以是多种多样的,比如 整数、字符串 等。
在定义 类 的时候可以给它一个/多个参数,这个/些参数表示不同的数据类型。在调用 类模板 时,指定参数,由编译系统根据参数提供的数据类型自动产生相应的 模板类。
下面看一个具体的例子,
template<class T> // 以template开头表示这是一个函数/类模板,后面跟着的class T表示这个类模板的参数
class Carray{
T *ptrElement;
int size;
public:
Carray(int length);
~ Carray();
int len();
void setElement(T arg, int index);
T getElement(int index);
};
在我们使用 函数模板 时可以直接使用 函数模板 而不必指定参数的类型。然而,在使用 类模板 时需要显式地指定参数的类型。
具体的使用方式如下,是在 类 后面加上类似于 就可以产生对应的 模板类。
Carray<int> arrayInt(50), *ptrArrayInt; // 创建一个元素类型为int的Carray模板类,并声明该模板类的一个对象以及一个指针
需要注意地是,不同的模板参数产生的 模板类 不是同一个 类,无法互相调用彼此的资源。
C++ 中使用 流 进行输入和输出。
输入和输出的过程就像一个连绵不断的串。当我们读取的时候,相当于从这个串中拿出一些内容而其后面的会向前顶;而当我们输出的时候,我们会在输出串的结尾流入一些新的内容。所以我们将之称为 流。
对一个变量 x,如果想从屏幕中读取相关的数据,我们可以使用 cin 来实现:
cin >> x;
此外,也可以用如下的 getline 来读取一整行:
cin.getline(str, len, ch); // 读入一个字符串。ch被从流中提出但不存入str。
// str 目标字符串,len 最长长度,ch 终止字符。读入结果会存入目标字符串str,如果读入字符串长度(str的长度)达到len或遇到终止字符ch,则结束读入
ch=cin.get(); // 读入一个单独的字符
cin.ignore(len, ch); // 忽略流中最近的一段长度达到len或者终止字符为ch的字符串
如何判断输入的结束呢?
int x;
while (cin >> x){
...
}
return 0;
当读入结束时会返回文件结束符 EOF。遇到这个字符我们就可以结束 while 循环,结束这个程序。
键盘读入时使用 ctrl-z 结束,相当于给出
EOF,文件读入时读到文件末尾。
可以使用如下的 cout 进行流的输出,
cout << y;
cout 输出到标准设备;
cerr 输出错误信息;
clog 输出错误日志;
输出单个字符可以使用如下的方式,
cout.put('A').put('a');
输出结果如下所示:
cout.put('A').put('a');
输出整型数,
int n = 10;
cout << n << endl;
cout << hex << n << endl
<< dec << n << endl
<< oct << n << endl;
输出结果如下所示:
10
a
10
12
输出浮点数,
#include
#include
using namespace std;
int main(){
double x=123567.89, y=12.34567;
int n=1234567;
int m=12;
cout << setprecision(6) << x << endl // 默认精度为6位有效数字
<< y << endl
<< setprecision(4) << y << endl
<< setprecision(3) << n << endl
<< m << endl;
cout << setiosflags(ios::fixed)
<< setprecision(6) << x << endl
<< y << endl
<< n << endl
<< m;
}
输出结果如下所示:
123568
12.3457
12.35
1234567
12
123567.890000
12.345670
1234567
12
setprecision(n),n 为正整数,对浮点数的所有有效数字位数进行控制;
setiosflags(ios::fixed),对浮点数的小数点后的有效数字位数进行控制;
当我们需要对文件进行操作时,输入、输出的方式和屏幕的标准输入 cin、输出方式 cout 基本是一样的,只不过我们需要用到文件流:
ifstream fin; // 输入文件流fin.open("input.txt", ios::in); // ios::in
fin >> ...
ofstream fout; // 输出文件流
fout.open("output.txt", ios::out); // ios::out 输出到文件,删除原有内容(默认打开文件选项)
// ios::app 输出到文件,保留原有内容,总是在尾部进行添加
// ios::ate 输出到文件,保留原有内容,可以在文件任意位置进行添加
fout << ...
在文件操作时,我们可以对文件指针进行操作移位,即可以跳过当前位置而跳到任意我们想要操作的位置。
以输出为例,通过 tellp 和 seekp 操作文件指针:
ofstream fout("a1.out", ios::ate);
long location = fout.tellp(); // 通过tellp获取写指针的位置
location = 10L;
fout.seekp(location); // 通过seekp将写指针移动到location(第10个字节)处
fout.seekp(location, ios::beg); // 通过seekp将写指针从头(ios::beg)移动到第location处
fout.seekp(location, ios::cur); // 通过seekp将写指针从当前位置(ios::cur)移动到第location处
fout.seekp(location, ios::end); // 通过seekp将写指针从尾部(ios::end)移动到第location处
对输入操作,文件指针则为 tellg、seekg