本章为基本数据结构的一些操作,很基础,当然也很重要。数据结构必须是泛型的,但是C对于泛型的支持只有通过void*强制转换,而且这也不是万能的,比较好的书籍可以参看《C interfaces and implementions》,当然这方面的代码是很成熟的。
ADT在C中的实现大致通过三种方式:静态数组、动态数组和链表。如果不使用指针,那么必须用宏来实现Stack_Type的泛型,push,pop和top的操作数类型都和Stack_Type关联。静态数组的stack实现就是简单的:
#define Stack_Type int #define Stack_Size 2048 //static array Stack_Type G_Stack_Node[Stack_Size]; void push(Stack_Type elem); Stack_Type pop(void); Stack_Type top(void);
由于引入了全局静态数组,带来了诸如溢出、导出不便的很多问题,所以一般不会采用这种方式。动态数组可以参照std::vector的内存管理方式,如:
1 #include <malloc.h> 2 #include <stdlib.h> 3 #include <assert.h> 4 5 #define Stack_Type int 6 typedef struct stack{ 7 size_t stack_size; 8 size_t stack_limit; 9 Stack_Type* stack_name; 10 }Stack; 11 12 Stack* stack_create(void); 13 { 14 Stack* s=malloc(sizeof(*s)); 15 assert(s); 16 s->size=0; 17 s->stack_limit=8; 18 s->stack_name=(Stack_Type*)malloc(8*sizeof Stack_Type); 19 } 20 void push(Stack* s,Stack_Type value) 21 { 22 if(s->stack_size==s->stack_limit) 23 { 24 Stack_Type* temp=s->stack_name; 25 s->stack_limit*=2; 26 s->stack_name=(Stack_Type*)malloc(s->stack_limit * sizeof(Stack_Type); 27 memcpy(s->stack_name,temp,s->stack_size * sizeof(Stack_Type); 28 free(temp); 29 } 30 s->stack_name[s->stack_size]=value; 31 s->stack_size+=1; 32 } 33 Stack_Type pop(Stack* s) 34 { 35 if(!s->stack_size)exit(EXIT_FAILTRUE); 36 Stack_Type temp=s->stack_name[--s->stack_size]; 37 return temp; 38 } 39 Stack_Type top(Stack* s) 40 { 41 if(!s->stack_size)exit(EXIT_FAILTRUE); 42 return s->stack_name[s->stack_size-1]; 43 } 44 void stack_free(Stack* s) 45 { 46 free(s.stack_name); 47 free(s); 48 }
还有就是采用链表的形式实现,需要定义链表结点,下面的实现没有依赖Stack_Type宏,而是使用了泛型指针:
1 struct node{ 2 void* value; 3 struct node* link; 4 }; 5 struct stack{ 6 node* stack_top; 7 size_t count; 8 }; 9 typedef struct stack* stack; 10 11 stack stack_create(void) 12 { 13 stack s=(stack)malloc(sizeof(stack)); 14 assert(s); 15 s->count=0; 16 s->stack_top=NULL; 17 return s; 18 } 19 void push(stack s,void* v) 20 { 21 struct node* tmp=s->stack_top; 22 s->stack_top=(node*)malloc(sizeof(node)); 23 s->stack_top->link=tmp; 24 s->stack_top->value=v; 25 s->count+=1; 26 } 27 void* top(stack s) 28 { 29 if(s->stack_top==NULL) 30 exit(EXIT_FAILTURE); 31 void* r=s->stack_top->value; 32 return r; 33 } 34 void* pop(stack s) 35 { 36 void* r=top(s); 37 s->stack_top=s->stack_top->link; 38 s->count-=1; 39 return r; 40 } 41 void stack_free(stack s) 42 { 43 while(s->stack_top) 44 { 45 node* tmp=s->stack_top; 46 s->stack_top=s->stack_top->link; 47 free(tmp); 48 } 49 free(s); 50 }
队列的实现和接口类似于栈,不同的是队列需要两个指针(或下标),分别指出队列尾部(超出末端的)和头部。如果基于链表来实现队列,不用考虑如何判定边界条件(也可以使用哨兵来简化边界条件),如果基于数组实现,不使用循环数组会造成空间浪费,使用循环数组要注意边界条件不能通过下标的大小关系简单判定。
链表的实现非常简单,在《Pointers on c》这本书里面还讲述了使用指向link字段的指针来简化搜索的技巧(使用指向结点的指针的指针而不是link字段作为循环更迭量)。
树的实现依赖于双链表,可以用两个指针表示任意子女数的树,方法是存放一个指向其最左子节点的指针和一个指向其右边兄弟的指针。
二叉树的遍历:
递归实现很简单,略过…非递归使用堆栈的话,主要有两种思想:
一种是考虑迭代,核心是“访问”。对于前序和中序,节点一旦访问完毕,就可以弹出了,因为我们能保证已弹出节点的所有子树都被访问过。对于后续遍历,弹出节点的同时必须有方法确认节点已经访问过,因为我们无法确定其右子树是否已经访问过(即是入栈访问后弹出还是压根没有入栈过)。所以下面后续遍历的版本1增加了一个变量来确认其右子树已经访问过。(没访问过就入栈右子树,访问过就可以弹出这个节点了)。
第二种是模拟堆栈,核心是“入栈”,即如何布置入栈顺序。比如前序的访问顺序是先根再左后右,那么入栈顺序就是相反的先右后左最后根,其他的类似推理。那么对于栈顶元素何时访问呢?对于前序,是无条件的,对于中序是其左子树已访问过,对于后序则是左右子树均访问过。因此需要一个标记位来确定是否已经访问过,没有访问过就入栈。
1 #include "stack.h" 2 bool is_visited(bin_tree* T) 3 { 4 if(!T)return true; //not existed is equal to visited 5 else return T->isvisited; 6 } 7 //使用迭代的思想 8 void preorder_traversal(bin_tree* T) 9 { 10 stack S; 11 while(T || !is_empty(S)) 12 { 13 if(T) 14 { 15 visit(T); 16 push(S,T); 17 T=left(T); 18 } 19 else 20 { 21 T=pop(S); 22 T=right(T); 23 } 24 } 25 //直接对递归进行模拟 26 //即仅考虑入栈和出栈顺序 27 //状态机 28 void preorder_traversal_ver2(bin_tree* T) 29 { 30 stack S; 31 if(T)push(S,T) 32 while(!is_empty(S)) 33 { 34 bin_tree* tmp=pop(S); 35 visit(tmp); 36 if(right(T))push(S,right(T)); 37 if(left(T))push(S,left(T)); 38 } 39 } 40 void inorder_traversal(bin_tree* T) 41 { 42 stack S; 43 while(T || !is_empty(S)) 44 { 45 while(T) 46 { 47 push(S,T); 48 T=left(T); 49 } 50 if(!is_empty(S)) 51 { 52 T=pop(S); 53 visit(T); 54 if(right(T))T=right(T); 55 } 56 } 57 } 58 59 void inorder_traversal_ver2(bin_tree* T) 60 { 61 stack S; 62 bin_tree* tmp; 63 if(T)push(S,T) 64 while(!is_empty(S)) 65 { 66 tmp=pop(S); //attention here. 67 if(!is_visited(left(tmp))) //left tree is existed and not visited 68 { 69 if(right(tmp))push(S,right(tmp)); 70 push(S,tmp); 71 push(S,left(tmp)); 72 } 73 else{ 74 visit(tmp); 75 set_visited(tmp); 76 if(!left(tmp) && right(tmp))push(S,right(tmp)); //left tree is not existed and right existed 77 } 78 } 79 } 80 void postorder_traversal(bin_tree* T) 81 { 82 stack S; 83 bin_tree* previsited=NULL; 84 while(T || !is_empty(S)) 85 { 86 while(T) 87 { 88 push(S,T); 89 T=left(T); 90 } 91 T=top(S); 92 if(!right(T) || right(T)==previsited) 93 { 94 visit(T); 95 previsited=T; 96 pop(S); 97 T=NULL; //to prevent from pushing left tree twice 98 }else 99 T=right(T); 100 } 101 } 102 103 void postorder_traversal_ver2(bin_tree* T) 104 { 105 stack S; 106 bin_tree* tmp; 107 if(T)push(S,T); 108 while(!is_empty(S)) 109 { 110 tmp=pop(S); 111 if(is_visited(right(tmp)) && is_visited(left(tmp))) //left and right both are visited 112 { 113 visit(tmp); 114 set_visited(tmp); 115 pop(S); 116 }else{ 117 if(!is_visited(right(tmp))) //right first 118 push(S,right(tmp)); 119 if(!is_visited(left(tmp))) //then left 120 push(S,left(tmp)); 121 } 122 } 123 }
当然除了链表外,也可以考虑使用其他技巧实现,比如堆。
本章的习题先略过了…