【算法导论】学习笔记——第10章 基本数据结构

基本数据结构主要包括:栈、队列、链表和有根树。

10.1 栈和队列
栈和队列都是动态集合,且在其上进行DELETE操作所移除的元素时预先设定的。在栈中,被删除的是最近插入的元素:栈实现的是一种后进先出(LIFO)策略。队列实现的是一种先进先出(FIFO)策略。

栈上的INSERT操作称为压入(PUSH),无参数的DELETE操作称为弹出(POP)。栈操作的代码非常简单:

 1 typedef struct {
 2     int A[MAXN];
 3     int top;
 4 } Stack_st;
 5
 6 int StackIsEmpty(Stack_st *S) {
 7     if (S->top == 0)
 8         return 1;
 9     return 0;
10 }
11
12 void Push(Stack_st *S, int x) {
13     if (S->top+1 < MAXN) {
14         S->top = S->top + 1;
15         S->A[S->top] = x;
16     }
17 }
18
19 int Pop(Stack_st *S) {
20     if ( StackIsEmpty(S) ) {
21         perror("underflow\n");
22         return -1;
23     } else {
24         --S->top;
25         return S->A[S->top+1];
26     }
27 }

三种栈操作的执行时间都是O(1)。
队列
队列上的INSERT操作称为入队(ENQUEUE),DELETE操作称为出队(DEQUEUE)。队列操作的代码也非常简单:

 1 typedef struct {
 2     int A[MAXN];
 3     int head, tail;
 4     int length;
 5 } Queue_st;
 6
 7 int QueueIsEmpty(Queue_st *Q) {
 8     if (Q->head == Q->tail)
 9         return 1;
10     return 0;
11 }
12
13 int QueueIsFull(Queue_st *Q) {
14     if ((Q->tail+1)%Q->length == Q->head)
15         return 1;
16     return 0;
17 }
18
19 void Enqueue(Queue_st *Q, int x) {
20     if (QueueIsFull(Q)) {
21         perror("overflow\n");
22         return ;
23     }
24     Q->A[Q->tail] = x;
25     if (Q->tail == Q->length)
26         Q->tail = 1;
27     else
28         ++Q->tail;
29 }
30
31 int Dequeue(Queue_st *Q) {
32     int x;
33
34     if (QueueIsEmpty(Q)) {
35         perror("underflow\n");
36         return -1;
37     } else {
38         x = Q->A[Q->head];
39         if (Q->head == Q->length)
40             Q->head = 1;
41         else
42             ++Q->head;
43         return x;
44     }
45 }

10.2 链表
链表(linked list)是一种这样的数据结构,其中的各对象按线性顺序排列。与数组不同的是,链表的顺序是由各个对象里的指针决定的。链表主要包括搜索、插入和删除操作。双向链表的操作代码如下。

 1 typedef struct Node {
 2     int key;
 3     struct Node *pre, *next;
 4 } Node;
 5
 6 typedef struct {
 7     Node *head;
 8 } List;
 9
10 Node *List_Search(List L, int k) {
11     Node *p = L.head;
12     while (p!=NULL && p->key!=k)
13         p = p->next;
14     return p;
15 }
16
17 void List_Insert(List *L, Node *x) {
18     x->next = L->head;
19     if (L->head != NULL)
20         L->head->pre = x;
21     L->head = x;
22     x->pre = NULL;
23 }
24
25 void List_Delete(List *L, Node *x) {
26     if (x->pre != NULL)
27         x->pre->next = x->next;
28     else
29         L->head = x->next;
30     if (x->next != NULL)
31         x->next->pre = x->pre;
32     free(x);
33 }

哨兵(sentinel)是一个哑对象,其作用是简化边界条件的处理。使用哨兵后,相关操作如下所示。

 1 typedef struct {
 2     Node *nil;
 3 } LList;
 4
 5 Node *LList_Search(LList L, int k) {
 6     Node *p = L.nil->next;
 7     while (p!=L.nil && p->key!=k)
 8         p = p->next;
 9     return p;
10 }
11
12 void LList_Insert(LList *L, Node *x) {
13     x->next = L->nil->next;
14     L->nil->next->pre = x;
15     L->nil->next = x;
16     x->pre = L->nil;
17 }
18
19 void LList_Delete(LList *L, Node *x) {
20     x->pre->next = x->next;
21     x->next->pre = x->pre;
22     free(x);
23 }

哨兵基本不能降低数据结构相关操作的渐近时间界,但可以降低常数因子。这哨兵其实就是《数据结构》里的头结点。

10.2-4 LIST_SEARCH‘过程中的每一次循环迭代都需要两个测试,一是检查x!=L.nil,另一个是检查x.key!=k试说明如何在每次迭代中省略对x!=L.nil的检查。
解:x!=L.nil主要是为了防止循环无限查找,为了省略x!=L.nil的检查,即需要在检查到尾结点可退出循环,即尾结点的下一个结点需要满足x.key==k,则进行搜索过程后,将哨兵的key值赋为k即可,代码实现如下:

1 Node *LList_Search(LList L, int k) {
2     Node *p = L.nil->next;
3     L.nil->key = k;
4     while (p->key!=k)
5         p = p->next;
6     return p;
7 }

10.2-6 选用合适的数据结构,支持O(1)时间的UNION操作。
解:双向循环链表(其实带不带哨兵均可,以带哨兵为例)。没什么技巧,就是把头尾结点该链的链好。

1 void LList_Union(LList *L, LList *L1, LList *L2) {
2     L->nil->pre = L2->nil->pre;
3     L->nil->next = L1->nil->next;
4     L1->nil->pre->next = L2->nil->next;
5     L2->nil->next->pre = L1->nil->pre;
6     L2->nil->pre->next = L->nil;
7     L1->nil->next->pre = L->nil;
8 }

10.2-7

时间: 2024-10-12 09:09:25

【算法导论】学习笔记——第10章 基本数据结构的相关文章

算法导论学习笔记——第10章 基本数据结构

栈 1 Stack-EMPTY(S) 2 if top[S]=0 3 then return TRUE 4 else return FALSE 5 6 PUSH(S,x) 7 top[S]←top[S]+1 8 S[top[S]]←x 9 10 POP(S) 11 if STACK-EMPTY(S) 12 then error "underflow" 13 else top[S]←top[S]-1 14 return S[top[S]+1] 队列 1 ENQUEUE(Q,x) 2 Q[

算法导论学习笔记——第12章 二叉查找树

二叉查找树性质 设x是二叉查找树中的一个结点,如果y是x的左子树中的一个结点,则k[y]<=key[x]:如果y是右子树中的一个结点,则k[y]>=k[x] 1 //中序遍历算法,输出二叉查找树T中的全部元素 2 INORDER-TREE-WALK(x) 3 if x!=nil 4 then INORDER-TREE-WALK(left[x]) 5 print key[x] 6 INORDER-TREE-WALK(right[x]) 查找 1 //递归版本 2 TREE-SEARCH(x,k)

算法导论学习笔记——第8章 线性时间排序

任意一种比较排序算法,在最坏情况下的运行时间下限是Ω(nlgn) 计数排序 假设n个输入元素中的每一个都是介于0到k之间的整数,k为某个整数,当k=O(n)时,计数排序的运行时间为Θ(n) 1 //输入数组A[1..n],存放排序结果数组B[1..n],临时存储区C[0..k] 2 COUNTING-SORT(A,B,k) 3 for i←0 to k 4 do C[i]←0 5 for j←1 to length[A] 6 do C[A[j]]←C[A[j]]+1 7 for i←1 to k

算法导论学习笔记 第7章 快速排序

对于包含n个数的输入数组来说,快速排序是一种时间复杂度为O(n^2)的排序算法.虽然最环情况的复杂度高,但是快速排序通常是实际应用排序中最好的选择,因为快排的平均性能非常好:它的期望复杂度是O(nlgn),而且O(nlgn)中的常数因子非常小.另外,快速排序还可以实现原址排序,甚至在虚拟环境中也能很好的工作. 1 快速排序的描述 与归并排序一样,快速排序也使用了分治法的思想,下面是对一个典型的子数组A[p.. r]进行快速排序的分治过长: 分解:数组A[p.. r]被划分为两个(可能为空)子数组

算法导论学习笔记——第1章

所谓算法,就是定义良好的计算过程,它取一个或一组值作为输入,并产生出一个或一组值作为输出.亦即,算法是一系列的计算过程,将输入值转换成输出值. 一些常见的算法运行时间量级比较:对数级<多项式级<指数级<阶乘级 1 lgn < n 1/2 < n < nlgn < n 2 < n 3 < 2 n < n!

算法导论学习笔记——第13章 红黑树

红黑树 红黑树是一种二叉查找树,但在每个结点上增加一个存储位存储结点的颜色,可以是red或black.通过对任意一条从根到叶的路径上结点颜色的限制,红黑树确保没有任何一条路径比其他路径长出两倍,因而是接近平衡的. 每个结点包含5个域,color,key,left,right,p 满足以下红黑性质: 1.每个结点是红色或黑色 2.根结点是黑色 3.每个叶结点(nil)是黑色 4.如果一个结点是红色,那么它的两个子结点都是黑色 5.对每个结点,从该结点到它每个子孙结点的路径上,黑结点数目相同 左旋转

算法导论学习笔记——第7章 快速排序

快速排序 1 QUICKSORT(A,p,r) 2 if p<r 3 then q←PARTITION(A,p,r) 4 QUICKSORT(A,p,q-1) 5 QUICKSORT(A,q+1,r) 6 7 PARTITION(A,p,r) 8 x←A[r] 9 i←p-1 10 for j←p to r-1 11 do if A[j]<=x 12 then i←i+1 13 exchange A[i]↔A[j] 14 exchange A[i+1]↔A[r] 15 return i+1 随

算法导论学习笔记——第11章 散列表

直接寻址表 1 DIRECT-ADDRESS-SEARCH(T,k) 2 return T[k] 3 4 DIRECT-ADDRESS-INSERT(T,x) 5 T[key[x]]←x 6 7 DIRECT-ADDRESS-DELETE(T,x) 8 T[key[x]]←nil

算法导论学习笔记——第6章

堆 堆数据结构是一种数组对象,可以被视为一棵完全二叉树. 对于给定的数组A,树的根为A[1],对于给定的下标为i的结点A[i],其父结点PARENT(i)=floor(i/2),左子结点LEFT(i)=2i,右子结点RIGHT(i)=2i+1 叶级结点的高度可以认为是0,每向上一层,高度加一,定义树的告诉为根结点的高度. P74