基本数据结构

众所周知, 数据结构分为以下四个方面 :

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 会向后移动, 此时, 前一个存储区域虽然出队列了, 但是仍然占据了存储空间没有释放, 这样就势必造成了空间的浪费, 这样最好的办法是使用循环队列, 但是循环队列如何判空和判满呢?

 image

如上图: 从结构上看, 队列里只剩下了 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 指向的是向量的首地址, 即数组.

image

可见 rhead 指向 行级指针数组, chead 指向 列级指针数组.

十字链表在做矩阵运算时非常方便.


- 树的双亲表示法

typedef struct treenode {

   int elem;

   int parent;

} PT;

typedef struct {

    PT nodes[MAX_TREE_SIZE];

    int r, n;  // 根结点和结点总数

} PTree;

image

结点: 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 就很麻烦.

image

- 树的孩子兄弟 表示法( 也叫二叉树表示法或二叉链表 表示法 ) 推荐

typedef struct CSNode {

    int elem;

    struct CSNode *firstchild, *nextsibling;  // 左孩子, 右兄弟

} CSNode, *CSTree;

结点: CSNode

关系: 首先定义一个结点为根结点, 然后利用 firstchild 指针指向第一个孩子, 依次继续, 具体结构图, 如下:

image

二叉树

- 顺序存储结构

typedef TelemType SqBiTree[MAX_TREE_SIZE];

SqBiTree bt;

结点: 存放在数组中.

关系: 通过结点存放在数组中的位置来判断结点之间的关系.

缺点: 浪费很多存储空间, 另外结点之间的关系不明显. 下图中黄颜色的全部是浪费的, 而且还有很多浪费的, 因为是按照完全二叉树的方式存储的.

image

- 二叉树, 二叉链表表示法

typedef struct BiNode {

    int elem;

    struct BiNode *leftChild, *rightChild;

} BiNode, *BiTree;

因为 二叉树的特点是最多只有2个儿子, 所以可以分为左右两个儿子, 然后进行存储.

结点: BiNode

关系: leftchild, rigthchild

可以看到, 这种方式的存储方法, 跟实际画图是一样的. 而且这种方式很像 左孩子又兄弟表示法, 这也是树与二叉树互换的依据.

image

- 二叉树, 三叉链表表示法

typedef struct BiNode {

    int elem;

    struct BiNode *leftChild, *rightChild, *parent;

} BiNode, *BiTree;

从定义上可以看出, 对边二叉链表表示法, 只是多了个指针指向 parent .

结点: BiNode

关系: leftchild, rightchild, parent

image

树的遍历

先序遍历: 根左右

中序遍历: 左根右

后续遍历: 左右根

森林与二叉树的转换

由于二叉树和树都可以用 二叉链表作为存储结构, 那么以二叉链表作为媒介可导出树与二叉树之间的一个对应关系, 从物理上, 他们的二叉表是相同的, 只是解释不同. 如下图:

image

image

原文地址:https://www.cnblogs.com/moveofgod/p/2967016.html