1.内存分配
所有的ADT(抽象数据类型)都必须确定意见事情--如何获取内存来存储值。有三种可选的方案:静态数组、动态分配的数组和动态分配的链式结构。静态数组要求结构的长度固定,而且这个长度必须在编译时确定,但这个方案最为简单且最不容易出错;动态分配需要在增加的复杂性和随之产生的灵活性之间作一番权衡;链式结构提供了最大程度的灵活性,每个元素在需要时才单独进行分配,所以除了不能超过机器的可用内存之外,这种方式对元素的数量几乎没有什么限制,但是链式结构的链接字段需要消耗一定的内存,在链式结构中访问一个特定元素的效率不如数组。
2.堆栈
堆栈最鲜明的特点就是后进先出(Last-In-First-Out,LIFO)的方式。
1)堆栈接口:push将新值压入到堆栈的顶部;pop把堆栈顶部的值移出堆栈并返回这个值;top返回顶部元素的值,但它并不把顶部元素从堆栈中移出。
2)实现堆栈
//一个堆栈模块的接口
#define STACK_TYPE int //堆栈所存储的值的类型
//push把一个新值压入到堆栈中,它的参数是需要被压入的值
void push(STACK_TYPE value)
//pop从堆栈弹出一个值,并将其丢弃
void pop(void)
//top返回堆栈顶部元素的值,但不对堆栈进行修改
STACK_TYPE top(void)
int is_empty(void) //如果堆栈为空,返回TRUE,否则返回FALSE
int is_full(void) //如果堆栈已满,返回TRUE,否则返回FALSE
a.数组堆栈
#include "stack.h"
#include<assert.h>
#define STACK_SIZE 100
static STACK_TYPE stack[STACK_SIZE];
static int top_element=-1;
void push(STACK_TYPE value){
assert(!is_full());
top_element+=1;
stack[top_element]=value;
}
void pop(void){
assert(!is_empty());
top_element-=1;
}
STACK_TYPE top(void){
assert(!is_empty());
return stack[top_element];
}
int is_empty(void){
return top_element==-1;
}
int is_full(void){
return top_elememt==STACK_SZIE-1;
}
b.动态数组堆栈
#include "stack.h"
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#include<assert.h>
static STACK_TYPE *stack;
static size_t stack_size;
static int top_element=-1;
void creat_stack(size_t size){
assert(stack_size==0);
stack_size=size;
stack=malloc(stack_size (sizeof(STACK_TYPE));
assert(stack!=NULL);
}
void destroy_stack(void){
assert(stack_size>0);
stack_size=0;
free(stack);
stack=NULL;
}
void push(STACK_TYPE value){
assert(!is_full());
top_element+=1;
stack[top_element]=value;
}
void pop(void){
assert(!is_empty());
top_element-=1;
}
STACK_TYPE top(void){
assert(!is_empty());
return stack[top_element];
}
int is_empty(void){
assert(stack_size>0);
return top_element==-1;
}
int is_full(void){
assert(stack_size>0);
return top_elememt==stack_size-1;
}
c.链式堆栈
#include "stack.h"
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#include<assert.h>
#define FALSE 0
typedef struct STACK_NODE{
STACK_TYPE value;
struct STACK_NODE *next;
}StackNode;
static StackNode *stack;
void creat_stack(size_t size){
}
void destroy_stack(void){
while(!is_empty())
pop();
}
void push(STACK_TYPE value){
StackNode *new_node;
new_node=malloc(sizeof(StackNode));
assert(new_node!=NULL);
new_node->value=value;
new_node->next=stack;
}
void pop(void){
StackNode *first_node;
assert(!is_empty());
first_node=stack;
stack=first_node->next;
free(first_node);
}
STACK_TYPE top(void){
assert(!is_empty());
return stack->value;
}
int is_empty(void){
return stack==NULL;
}
int is_full(void){
return FALSE;
}
3.队列
队列与堆栈不同,队列是一种先进先出(First-IN First-OUT,FIFO)的结构。
1)队列接口
//一个队列模块的接口
#include<stdlib.h>
#define QUEUE_TYPE int //队列所存储的值的类型
void create_queue(size_t size);
void destroy_queue(void);
void insert(QUEUE_TYPE value);
void delete(void);
QUEUE_TYPE first(void);
int is_empty(void) //如果队列为空,返回TRUE,否则返回FALSE
int is_full(void) //如果队列已满,返回TRUE,否则返回FALSE
2)实现队列
队列的实现比堆栈要难得多,它需要两个指针——一个指向队头,一个指向队尾。同时数组并不像适合堆栈那样适合队列实现,这是由于队列使用内存的方式决定的。
a.数组队列
#include"queue.h"
#include<stdio.h>
#include<assert.h>
#define QUEUE_TYPE 100
#define ARRAY_SIZE {QUEUE_SIZE+1}
static QUEUE_TYPE queue{ARRAY_SIZE};
static size_t front=1;
static size_t rear=0;
void insert(QUEUE_TYPE value){
assert(!is_full());
rear=(rear+1)%ARRAY_SIZE;
queue[rear]=value;
}
void delete(void){
assert(!is_empty());
front=(front+1)%ARRAY_SIZE;
}
QUEUE_TYPE first(void){
assert(!is_empty());
return queue[front];
}
int is_empty(void){
return (rear+1)%ARRAY_SIZE==front;
}
int is_full(void){
return (rear+2)%ARRAY_SIZE==front;
}
4.树
本文主要阐述二叉搜索树:每个节点的值比它的左子树的所有结点的值都要大,比它的右子树的所有结点的值都要小。
1)在二叉搜索树中插入
如果树为空:
把值作为根结点插入
否则:
如果新值小于当前节点的值
把新值插入到当前节点的左子树
否则:
把新值插入到当前节点的右子树
2)在二叉搜索树删除结点
树的删除将导致它的子树和树的其余部分断开——必须重新连接起来,否则会丢失,因此树的删除比队列或堆栈更困难。必须处理的三种情况:a)删除没有孩子的节点;b)删除只有一个孩子的节点;c)删除有两个孩子的节点。
3)在二叉搜索树中查找
如果树为空:
这个值不存在于树中
否则:
如果这个值和根节点的值相等:
成功找到这个值
否则:
如果这个值小于根节点的值:
查找左子树
否则:
查找右子树
4)树的遍历
树的遍历:前序(pre-order)、中序(in-order)、后续(post-order)和层次遍历(breadth-first)。前序遍历检查节点的值,然后递归地遍历左子树和右子树;中序遍历首先遍历左子树,然后检查当前节点的值,最后遍历右子树;后序遍历首先遍历左右子树,然后检查当前节点的值;层次遍历逐层检查树的节点,首先处理根节点,接着是它的孩子,再接着是它的孙子,依次类推。
前序遍历结果为:20,12,5,9,16,17,25,28,26,29
中序遍历结果为:5,9,12,16,17,20,25,26,28,29
后序遍历结果为:9,5,17,16,12,26,29,28,25,20
层次遍历结果为:20,12,25,5,16,28,9,17,26,29
虽然前三种遍历方法可以很容易地使用递归来实现,但最后这种层次遍历要采用一种使用队列的迭代算法。
5)二叉搜索树接口
#define TREE_TYPE int //树的值类型
void insert(TERR_TYPE value);
TREE_TYPE *find(TREE_TYPE value);
void pre_order_traverse(void(*callback)(TREE_TYPE value));
6)实现二叉搜索树
a.数组形式的二叉搜索树
结点N的双亲节点是节点(N+1)/2-1,结点N的左孩子节点是节点2N+1,节点N的右孩子节点是节点2N+2。
//一个使用静态数组实现的二叉搜索树,数组的长度只能通过修改#define定义
#include"tree.h"
#include<assert.h>
#include<stdio.h>
#define TREE_SIZE 100
#define ARRAY_SIZE (TREE_SIZE+1)
//用于存储树的所有节点的数组
static TREE_TYPE tree[ARRAY_SIZE];
static int left_child(int current){
return current*2;
}
static int right_child(int current){
return current*2+1
}
void insert(TREE_TYPE value){
int current;
assert(value!=0);
current=1;//从根节点开始
while(tree[current]!=0){
if(value<tree[current])
current=left_child(current);
else{
assert(value!=tee[current]);
current=right_child(current);
}
assert(current<ARRAY_SIZE);
}
tree[current]=value;
}
TREE_TYPE *find(TREE_TYPE value){
int current;
current=1;
while(current<ARRAY_SIZE && tree[current]!=value){
if(value<tree[current])
current=left_child(current);
else
current=right_child(current);
}
if(current<ARRAY_SIZE)
return tree+current;
else
return 0;
}
static void do_pre_order_traverse(int current,void (*callback)(TREE_TYPE value)){
if(current<ARRAY_SIZE && tree[current]!=0){
callback(tree[current]);
do_pre_order_traverse(left_child(current),callback);
do_pre_order_traverse(right_child(current),callback);
}
}
void pre_order_traverse(void(*callback)(TREE_TYPE value)){
do_pre_order_traverse(1,callback);
}
b.链式二叉搜索树
//一个使用动态分配的链式结构实现二叉搜索树
#include"tree.h"
#include<assert.h>
#include<stdio.h>
#include<malloc.h>
typedef struct TREE_NODE{
TREE_TYPE value;
struct TREE_NODE *left;
strunt TREE_NODE *right;
}TreeNode;
//指向树根节点的指针
static TreeNode *tree;
void insert(TREE_TYPE value){
TreeNode *current;
TreeNode **link;
//从根节点开始
link=&tree;
//持续查找值,进入合适的子树
while((current=*link)!=NULL){
if(value<current->value)
link=¤t->left;
else{
assert(value!=current->value);
link=¤t->right;
}
}
}
//分配一个新节点,使适当节点的link字段指向它.
current=malloc(sizeof(TreeNode));
assert(current!=NULL);
current->value=value;
current->left=NULL;
current->right=NULL;
*link=current;
TREE_TYPE *find(TREE_TYPE value){
TreeNode *current;
current=tree;
while(current!=NULL && current!=value){
if(value<current->value)
current=current->left;
else
current=current->right;
}
if(current!=NULL)
return ¤t->value;
else
return NULL;
}
static void do_pre_order_traverse(int current,void (*callback)(TREE_TYPE value)){
if(current!=NULL){
callback(current->value);
do_pre_order_traverse(current->left,callback);
do_pre_order_traverse(current->right,callback);
}
}
void pre_order_traverse(void(*callback)(TREE_TYPE value)){
do_pre_order_traverse(tree,callback);
}
5.实现的改进
拥有超过一个的堆栈,拥有超过一种的类型,名字冲突,标准函数库的ADT。
6.总结
为ADT分配内存有三种技巧:静态数组、动态分配的数组和动态分配的链式结构;堆栈是一种后进先出的结构;队列是一种先进先出的结构;二叉搜索树(BST)是一种数据结构,它或者为空,或者具有一个值并拥有零个、一个或两个孩子(左右孩子),它的孩子本身也是一棵BST。
7.警告的总结
1)使用断言检查内存是否分配成功是危险的;
2)数组形式的二叉树节点位置计算公式假定数组的下标从1开始;
3)把数据封装于对它进行操纵的模块可以防止用户不正确地访问数据;
4)与类型无关的函数没有类型检查,所以应该小心,确保传递正确类型的数据;
8.编程提示总结
1)避免使用具有副作用的函数可以使程序更容易理解;
2)一个模块的接口应该避免暴露它的实现细节;
3)将数据类型参数化,使它更容易修改;
4)只有模块对外公布的接口才应该是公用的;
5)使用断言来防止非法操作;
6)几个不同的实现使用同一个通用接口使模块具有更强的可互换性;
7)复用现存的代码而不是对它进行改写;
8)迭代比尾部递归效率更高;