C/C++知识总结
第一章 C语言基础知识
第二章 C语言高级编程
第三章 C语言数据结构
文章目录
前言
数据结构是计算机存储、组织数据的方式,是相互之间存在一种或多种特定关系的数据元素集合。而算法是特定问题求解步骤的描述,在计算机中表现为指令的有限序列,算法是独立存在的一种解决问题的方法和思想。
算法的五个特性:输入、输出、有穷性、确定性、可行性。
数据结构分类:
逻辑结构:集合、线性、树形、图形
物理结构:顺序存储、链式存储
一、线性表
- 线性结构的基本特点是节点之间满足线性关系。动态数组、链表、栈、队列都属于线性结构,他们的共同之处是节点中有且只有一个开始节点和终端节点。
- 线性表是零个或者多个数据元素的有限序列,数据元素之间是有顺序的,数据元素个数是有限的,数据元素的类型必须相同。
- 线性表的性质:
1)a0 为线性表的第一个元素,只有一个后继。
2)an 为线性表的最后一个元素,只有一个前驱。
3)除 a0 和 an 外的其它元素 ai,既有前驱,又有后继。
4)线性表能够逐项访问和顺序存取。 - 线性表的抽象数据类型定义:
ADT线性表(List)
Data
线性表的数据对象集合为{ a1, a2, ……, an },每个元素的类型均为DataType。
其中,除第一个元素a1外,每个元素有且只有一个直接前驱元素.
除了最后一个元素an外,每个元素有且只有一个直接后继元素。数据元素之间的关系是一一对应的。
Operation(操作)
// 初始化,建立一个空的线性表L。
InitList(*L);
// 若线性表为空,返回true,否则返回false
ListEmpty(L);
// 将线性表清空
ClearList(*L);
// 将线性表L中的第i个位置的元素返回给e
GetElem(L, i, *e);
// 在线性表L中的第i个位置插入新元素e
ListInsert(*L, i, e);
// 删除线性表L中的第i个位置元素,并用e返回其值
ListDelete(*L, i, *e);
// 返回线性表L的元素个数
ListLength(L);
// 销毁线性表
DestroyList(*L);
1.1 线性表顺序存储(动态数组)的设计与实现
- 采用顺序存储是表示线性表最简单的方法,具体做法是:将线性表中的元素一个接一个的存储在一块连续的存储区域中,这种顺序表示的线性表也成为顺序表。
- 动态数组实现:无法确定用户数据类型;无法确定数据分配的具体位置;不管创建在哪,不管什么数据类型,放在内存中都会有有数据的地址。
struct dynamicArray
属性:
void ** pAddr 维护真实在堆区创建的数组的指针
int m_capacity; 数组容量
int m_size; 数组大小
- 动态数组操作:初始化、插入、遍历、按位置删除、按值删除、销毁。
- 插入元素:插入元素时,所有元素都向后移动,线性表长度加1。删除元素:将删除位置后的元素分别向前移动一个位置,线性表长度减1。获取元素:直接通过数组下标的方式获取元素。
- 动态数组的优缺点:
无需为线性表中的逻辑关系增加额外的空间。
可以快速的获取表中合法位置的元素。
插入和删除操作需要移动大量元素(缺点)。
1.2 线性表的链式存储(单向链表)的设计与实现
- 动态数组最大的缺点是插入和删除时需要移动大量元素,链表为了表示每个数据元素与其直接后继元素之间的逻辑关系,每个元素除了存储本身的信息外,还需要存储指示其直接后继的信息。
- 单链表实现:线性表的链式存储结构中,每个节点中只包含一个指针域,这样的链表叫单链表。通过每个节点的指针域将线性表的数据元素按其逻辑次序链接在一起。头结点:链表中的第一个结点,包含指向第一个数据元素的指针以及链表自身的一些信息。
//节点结构体
struct LinkNode
{
//数据域
void* data;
//指针域
struct LinkNode* next;
};
//链表结构体
struct LList
{
//头节点
struct LinkNode pHeader;
//链表长度
int m_size;
};
//隐藏属性 防止用户直接修改链表长度和头节点
typedef void* LinkList;
- 单链表的操作:初始化、插入、遍历、按位置删除、按值删除、清空、返回链表长度、销毁。
- 插入操作:先牵新绳,再解旧绳。
node->next = current->next;
current->next = node;
- 删除操作:
current->next = ret->next;
- 链表的优缺点:
无需一次性定制链表的容量 。
插入和删除操作无需移动数据元素。
数据元素必须保存后继元素的位置信息(缺点)。
获取指定数据的元素操作需要顺序访问之前的元素(缺点)。 - 链表企业版:节点只维护指针域,用户数据预留前4个字节由底层使用。
//节点结构体
struct LinkNode
{
//只维护指针域
struct LinkNode* next;
};
//链表结构体
struct LList
{
struct LinkNode pHeader;
int m_Size;
};
typedef void* LinkList;
二、受限线性表
2.1 栈
- 栈元素具有线性关系,即前驱后继关系,是一种特殊的线性表。只在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。它始终只在栈顶进行,这也就使得:栈底是固定的,最先进栈的只能在栈底。
- 操作:栈的插入操作,叫做进栈,也成压栈,类似子弹入弹夹。栈的删除操作,叫做出栈,也有的叫做弾栈,退栈,如同弹夹中的子弹出夹。此外还有:栈是否为空、返回栈顶元素、返回栈大小、销毁栈。
- 栈不可以遍历,遍历需要访问容器中所有元素,且访问之后元素不会发生改变。
- 栈的顺序存储结构简称顺序栈,它是运算受限制的顺序表。顺序栈的存储结构是:利用一组地址连续的的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top只是栈顶元素在顺序表中的位置。
- 利用数组模拟出先进后出数据结构,数组中首地址做栈底方便数组尾部做插入删除。栈顶需要频繁作入栈、出栈操作,对于数组尾部插入和删除效率高。
- 栈的链式存储结构简称链栈。由于单链表有头指针,而栈顶指针也是必须的,所以比较好的办法就是把栈顶放在单链表的头部。另外都已经有了栈顶在头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的。
- 利用链表模拟出先进后出的数据结构,头节点端做栈顶比较方便做入栈和出栈。
- 栈的应用:就近匹配、中缀表达式转后缀表达式。
- 就近匹配:实现编译器中的符号成对检测。
从第一个字符开始扫描所有字符。
遇到普通字符 直接忽略。
遇到左括号,入栈。
遇到右括号:
如果栈中有元素,出栈。
如果栈中没有元素立即停止,并且报错。
当所有字符都扫描完毕,查看栈中内容:
如果是空栈,没有问题。
如果不是空栈,报错。
- 中缀表达式转后缀表达式:
- 遍历中缀表达式中的数字和符号:
5 + 4 => 5 4 +
1 + 2 * 3 => 1 2 3 * +
8 +( 3 – 1 ) * 5 => 8 3 1 – 5 * +
对于数字:直接输出
对于符号:
左括号:进栈
运算符号:与栈顶符号进行优先级比较
若栈顶符号优先级低:此符号进栈
(默认栈顶若是左括号,左括号优先级最低)
若栈顶符号优先级不低:将栈顶符号弹出并输出,之后进栈
右括号:将栈顶符号弹出并输出,直到匹配左括号,将左括号和右括号同时舍弃
遍历结束:将栈中的所有符号弹出并输出
- 基于后缀表达式运算:
遍历后缀表达式中的数字和符号
对于数字:进栈
对于符号:
从栈中弹出右操作数
从栈中弹出左操作数
根据符号进行运算
将运算结果压入栈中
遍历结束:栈中的唯一数字为计算结果
2.2 队列
- 队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出的(First In First Out)的线性表,简称FIFO。
- 允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作!假设队列是q=(a1,a2,……,an),那么a1就是队头元素,而an是队尾元素。这样我们就可以删除时,总是从a1开始,而插入时,总是在队列最后。
- 操作:初始化、入队、出队、返回队列大小、判断是否为空、队头元素、队尾元素、销毁队列。
- 队列不提供遍历功能。只有队头和队尾才能被外界访问到。
- 队列的顺序存储:利用数组模拟出先进先出的数据结构,数组的首地址作队头或队尾都可以,因为总要在数组一端作插入或者删除操作。
- 队列的链式存储:利用链表模拟出先进先出数据结构,节点只维护指针域,头结点作队头或队尾都可以,因为总要在头尾进行操作,所有队头和队尾都可以。
三、树和二叉树
3.1 树的概念
- 树的基本概念:由一个或多个(n≥0)结点组成的有限集合T,有且仅有一个结点称为根(root),当n>1时,其余的结点分为m(m≥0)个互不相交的有限集合T1,T2,…,Tm。每个集合本身又是棵树,被称作这个根的子树 。
- 特点:非线性结构,有一个直接前驱,但可能有多个直接后继(1:n);树的定义具有递归性,树中还有树;树可以为空,即节点个数为0。
- 术语:根、叶子、森林、有序、无序等。
根:即根结点(没有前驱)
叶子:即终端结点(没有后继)
森林:指 m 棵不相交的树的集合(例如删除A后的子树个数)
有序树:结点各子树从左至右有序,不能互换(左为第一)
无序树:结点各子树可互换位置。
双亲:即上层的那个结点(直接前驱) parent
孩子:即下层结点的子树 (直接后继) child
兄弟:同一双亲下的同层结点(孩子之间互称兄弟)sibling
堂兄弟:即双亲位于同一层的结点(但并非同一双亲)cousin
祖先:即从根到该结点所经分支的所有结点
子孙:即该结点下层子树中的任一结点
结点:即树的数据元素
结点的度:结点挂接的子树数(有几个直接后继就是几度)
结点的层次:从根到该结点的层数(根结点算第一层)
终端结点:即度为0的结点,即叶子
分支结点: 除树根以外的结点(也称为内部结点)
树的度:所有结点度中的最大值(Max{各结点的度})
树的深度(或高度):指所有结点中最大的层数(Max{各结点的层次})
- 上图中的结点数= 13,树的度= 3,树的深度= 4。
- 树的表示法:广义表表示(根作为由子树森林组成的表的名字写在表的左边)
中国(河北(保定,石家庄),广东(广州,东莞),山东(青岛,济南)) - 左孩子右兄弟表示法:可以将一颗多叉树转化为一颗二叉树。节点有两个指针域,其中一个指针指向子节点,另一个指针指向其兄弟节点。
3.2 二叉树概念
- 基本概念:n(n≥0)个结点的有限集合,由一个根结点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成 。一对二的逻辑结构。特征为每个结点最多只有两棵子树(不存在度大于2的结点),左子树和右子树次序不能颠倒(有序树)。
- 满二叉树:一棵深度为k 且有 2 k − 1 2^k -1 2k−1个结点的二叉树,每层都“充满”了结点。完全二叉树:除最后一层外,每一层上的节点数均达到最大值,在最后一层上只缺少右边的若干结点。k-1层与满二叉树完全相同,第k层结点尽力靠左。
|
|
- 二叉树性质:
性质1: 在二叉树的第i层上至多有2^(i-1)个结点(i>0)
性质2: 深度为k的二叉树至多有2^k-1个结点(k>0)
性质3: 对于任何一棵二叉树,若度为2的结点数有n2个,则叶子数(n0)必定为n2+1 (即n0=n2+1)
性质4: 具有n个结点的完全二叉树的深度必为[log2n] + 1 (向下取整)
性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,
其右孩子编号必为2i+1;其双亲的编号必为i/2(i=1 时为根,除外)
- 利用性质5可以使用完全二叉树实现树的顺序存储,如果不是完全二叉树,将其转换成完全二叉树即可。
|
|
- 二叉树的表示:二叉链表示法:每个节点有两个指针域,其中分别指向子节点(左孩子,右孩子)。三叉链表表示:每个节点有三个指针域,其中两个分别指向子节点(左孩子,右孩子),还有一共指针指向该节点的父节点。
//二叉链表
typedef struct BiTNode
{
int data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
//三叉链表
typedef struct TriTNode
{
int data;
//左右孩子指针
struct TriTNode *lchild, *rchild;
struct TriTNode *parent;
}TriTNode, *TriTree;
- 二叉树的遍历:遍历是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。对每个结点的查看都是“先左后右”,限定先左后右,树的遍历有三种实现方案:先 (根)序遍历,中 (根)序遍历,后(根)序遍历。从递归的角度看,这三种算法是完全相同的,或者说这三种遍历算法的访问路径是相同的,只是访问结点的时机不同。
- 求二叉树序列:已知先序遍历序列和中序遍历序列,可求出后序序列或者已知中序序列和后序序列,可求出前序序列。但已知先序序列和后序序列,无法唯一确定一棵树,无法得知中序序列。
- 二叉树操作:求二叉树叶子数量、求二叉树高度、拷贝二叉树、释放二叉树。
左子树与右子树都同时为NULL,称为叶子。
左子树高度与右子树高度比,取大的值 +1 就是这个树的高度。
先拷贝左子树,再拷贝右子树,再创建根节点,挂载拷贝出的左右子树,返回给用户。
利用递归特性释放二叉树。
- 二叉树的非递归遍历:利用栈容器可以实现二叉树的非递归遍历。首先将每个节点都设置一个标志,默认标志为假,根据节点的的状态进行如下流程,可以得到先序遍历的结果,如果想得到其他二叉树遍历结果,修改2.4步骤即可。
1.将根节点 入栈
2.只要栈中元素个数大于 0 执行循环
2.1 获取栈顶元素,出栈
2.2 如果标志位真 直接输出 并且执行下一次循环
2.3 如果为假 将标志改为真
2.4 将右子树 左子树 根 入栈
2.5 执行下一次循环
总结
本文主要对线性表、栈和队列、树与二叉树的数据结构实现做了介绍,后续文章会介绍更加复杂的数据结构和算法。
笔记来源:黑马程序员C语言课程