基本数据结构
众所周知, 数据结构分为以下四个方面 :
1. 集合 ( 结点之间没什么联系, 不需要总结 )
2. 线性 ( 一条直线 )
3. 树状 ( 类似家谱 )
4. 图 ( 难, 暂时先不总结 )
数据结构的定义: 分为结点的定义和结点之间关系的定义.
线性结构
- 顺序表
typedef struct {
int elem[100];
int length; // 这里的lenth是指当前分配的长度
} SqList;
由以上结构可以看出, 结点的值存储在 elem 中,而结点之间的关系就是数组隐含, 所以不需要另外在定义关系.
- 单链表
typedef struct LNode{
int elem;
struct LNode *next;
} LNode, *LinkList;
结点: LNode 是用来保存结点的
关系: LinkList 就是链表头指针, 关系是通过 next 指针联系起来的.
头指针: LinkList 就是头指针, 指向头结点的指针.
头结点: (1)对带头结点的链表, 在表的任何结点之前插入结点或删除表中任何结点, 所要做的都是修改前一个结点的指针域, 而任何元素都有前驱结点, 若链表没有头结点, 则首元素结点没有前驱结点, 在其前插入结点或删除结点时操作会复杂些.(2)对带头结点的链表, 表头指针时指向结点的非空指针, 因此空表与非空表处理是一样的.
- 循环链表
所谓循环链表, 其实
结点: 存储情况, 同上边完全一样.
关系: 头指针的 next 指向自己, 这样的话就是循环链表了, 当插入结点时, 新的结点 p->next = L->next(指向头结点).
- 双向链表
typedef struct DLNode {
int elem;
struct DLNode *prior;
struct DLNode *next;
} DLNode, *DLinkList;
结点: DLNode
结构: 头指针 DLinkList, 通过 next 和 prior 来反映元素之间的线性关系.
- 静态链表
所谓静态链表: 是指用数组模拟操作, 实现的链表, 其中指针域, 使用数组下标表示.
typedef struct {
int elem;
int next;
} SLNode, slinklist[MAXSIZE];
结点: SLNode, 其中的 next 就是模拟指针.
关系: slinklist 是一个SLNode的数组, 数组中的 next 隐含关系.
栈和队列: 是限制操作的线性表
- 顺序栈
typedef struct {
int elem[100];
int top;
} SqStack;
结点: 数组中的元素;
关系: SqStack.
为什么没有链式栈, 因为栈这种结构限制了, 后进先出, 即只能从栈顶出战, 即 top 会记录栈顶位置, 所以它虽然是顺序结构, 但是插入和删除操作并不需要移动元素, 所以, 当然是顺序栈好一些.
- 顺序队列( 循环队列 )
typedef struct {
int elem[100];
int front;
int rear;
} SqQueue;
结点: elem数组中的元素
关系: 隐含在数组中, 注意 front 和 rear 的位置, 关系还是隐含在数组中, 队列是先进先出, front 记录了队列头, rear 记录了队列尾, 从 front出, rear进, 注意队列判空和判满条件: 如下
因为 出队列时, 头指针 front 会向后移动, 此时, 前一个存储区域虽然出队列了, 但是仍然占据了存储空间没有释放, 这样就势必造成了空间的浪费, 这样最好的办法是使用循环队列, 但是循环队列如何判空和判满呢?
如上图: 从结构上看, 队列里只剩下了 3 个存储单元, 前边浪费了大量存储空间, 所以要使用循环队列, 并且不能通过 front == rear 来简单的判断判空或判满, 浪费一个存储空间, 即 (rear + 1) % 存储空间 = front, 则判断为慢, 关键看谁最上谁, 如果 front追上rear 空队列, 如果是 rear追上了 front满队列. 为什么要 (rear + 1)%存储空间呢? 因为当 rear已经在数组最右边时, 如果单纯的 rear+1, 那么已经超过数组最大范围, 但是(rear+1)%存储空间, 如果 rear+1没有超过存储空间, 那么取模与不取模操作都一样, 但是如果 rear+1超过了数组范围,那么取模以后, 又回到了第一个了, 这样就达到了循环的目的, 而 (rear+1)%存储空间 == front 表示 rear 已经循环到了 front的前一个存储空间了.
- 链式队列
typedef struct Qnode {
int elem;
struct Qnode *next;
} Qnode, *Qlink;
typedef struct SQlink {
Qlink front;
Qlink rear;
} *linkqueue;
结点: Qnode
关系: linkqueue
注意: 链式队列不需要循环队列, 因为不存在空间浪费的情况, 当有出队列的结点时, 直接释放该结点的内存就可以了.
数组相关, 矩阵压缩存储
- 三元组
typedef struct {
int i, j; // 非零元 的行和列
int elem;
} Tripe;
typedef struct {
Tripe Matrix[MAX_SIZE];
int mu, nu, tu; // 矩阵的行, 列数, 及非零元个数
} TMatrix;
结点: Tripe;
关系: TMatrix
特点: 非零元在数组中按行逻辑顺序存储便于进行依次顺序处理矩阵运算, 但是, 如果我想找到一行的非零元, 就比较麻烦, 还是需要从头开始找, 由此引出 行逻辑链接顺序表存储法.
- 行逻辑链接顺序表
typedef struct {
int i, j; // 非零元 的行和列
int elem;
} Tripe;
typedef struct {
Tripe Matrix[MAX_SIZE];
int mu, nu, tu; // 矩阵的行, 列数, 及非零元个数
int rpos[MAXRC+1]; // 各行第一个非零元的位置表
} LMatrix;
结点: Tripe
关系: LMatrix
这个存储结构跟三元组基本上一样, 只是多了一个记录在数组中, 第几个元素还是是第几行的开始非零元. 这里的 rpos[MAXRC+1] 记录的是第几行在数组中的非零元的起始位置, 例如 rpos[2] = 5 表示 第 2 行非零元的起始位置, 在Matrix=[5]
- 十字链表存储发
以上的存储方式, 说白了, 还是顺序存储, 如果矩阵非零元个数和位置变化较大, 就比较适合使用链式存储结构.
typedef struct mxtripe {
int elem;
int i, j;
struct mxtripe *right;
struct mxtripe *end;
}MxTripe, *OLink;
typedef struct {
OLink *rhead; // rhead 指向的是一个行向量, 该向量指向 元素类型
OLink *chead; // chead 指向的是一个列向量, 该向量指向 元素类型
int mu, nu, tu;
}CrossList;
rhead, chead 指向的是向量的首地址, 即数组.
可见 rhead 指向 行级指针数组, chead 指向 列级指针数组.
十字链表在做矩阵运算时非常方便.
- 树的双亲表示法
typedef struct treenode {
int elem;
int parent;
} PT;
typedef struct {
PT nodes[MAX_TREE_SIZE];
int r, n; // 根结点和结点总数
} PTree;
结点: PT
关系: PTree, 其中关系也是隐含在结点的 parent中.
这种存储方式, 很显然, 找儿子特别困难. 找parent相对容易.
- 树的孩子链表 表示法
typedef struct CTNode { // 孩子结点, 此节点如果缺少 child, 保存信息并不完整
int child; // 在数组中的下标
struct CTNode *next;
} *ChildPtr;
typedef struct { // 树中的结点
int data;
ChildPtr firstchild; // 孩子链表头指针
} CTBox;
typedef struct { // 树结构
CTBox nodes[MAX_TREE_SIZE];
int n,r; // 结点数 和 根位置( 在数组中 )
} CTree;
此种结构, 找到孩子很容易, 但是由孩子找 parent 就很麻烦.
- 树的孩子兄弟 表示法( 也叫二叉树表示法或二叉链表 表示法 ) 推荐
typedef struct CSNode {
int elem;
struct CSNode *firstchild, *nextsibling; // 左孩子, 右兄弟
} CSNode, *CSTree;
结点: CSNode
关系: 首先定义一个结点为根结点, 然后利用 firstchild 指针指向第一个孩子, 依次继续, 具体结构图, 如下:
二叉树
- 顺序存储结构
typedef TelemType SqBiTree[MAX_TREE_SIZE];
SqBiTree bt;
结点: 存放在数组中.
关系: 通过结点存放在数组中的位置来判断结点之间的关系.
缺点: 浪费很多存储空间, 另外结点之间的关系不明显. 下图中黄颜色的全部是浪费的, 而且还有很多浪费的, 因为是按照完全二叉树的方式存储的.
- 二叉树, 二叉链表表示法
typedef struct BiNode {
int elem;
struct BiNode *leftChild, *rightChild;
} BiNode, *BiTree;
因为 二叉树的特点是最多只有2个儿子, 所以可以分为左右两个儿子, 然后进行存储.
结点: BiNode
关系: leftchild, rigthchild
可以看到, 这种方式的存储方法, 跟实际画图是一样的. 而且这种方式很像 左孩子又兄弟表示法, 这也是树与二叉树互换的依据.
- 二叉树, 三叉链表表示法
typedef struct BiNode {
int elem;
struct BiNode *leftChild, *rightChild, *parent;
} BiNode, *BiTree;
从定义上可以看出, 对边二叉链表表示法, 只是多了个指针指向 parent .
结点: BiNode
关系: leftchild, rightchild, parent
树的遍历
先序遍历: 根左右
中序遍历: 左根右
后续遍历: 左右根
森林与二叉树的转换
由于二叉树和树都可以用 二叉链表作为存储结构, 那么以二叉链表作为媒介可导出树与二叉树之间的一个对应关系, 从物理上, 他们的二叉表是相同的, 只是解释不同. 如下图: