眾所周知, 數據結構分為以下四個方面 :
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
樹的遍歷
先序遍歷: 根左右
中序遍歷: 左根右
后續遍歷: 左右根
森林與二叉樹的轉換
由於二叉樹和樹都可以用 二叉鏈表作為存儲結構, 那么以二叉鏈表作為媒介可導出樹與二叉樹之間的一個對應關系, 從物理上, 他們的二叉表是相同的, 只是解釋不同. 如下圖: