0
点赞
收藏
分享

微信扫一扫

(第17章)经典抽象数据类型


文章目录

  • ​​1.内存分配​​
  • ​​2.堆栈​​
  • ​​3.队列​​
  • ​​4.二叉搜索树​​

1.内存分配

  • ADT:抽象数据类型
  • 所有的ADT都必须确定一件事:如何获取内存来存储值,有三个方案:
    (1)静态数组:长度固定,长度在编译的时候决定
    (2)动态数组:在运行时,决定数组的长度
    (3)链式结构:每个元素在需要时才单独进行分配,链式结构的链接字段需要消耗一定的内存,在链式结构中访问一个特定元素的效率不如数组

2.堆栈

  • 堆栈stack特点:后进先出(List-In First-Out,LIFO),三种基本操作如下:
    (1)push:把一个新值压入到堆栈的顶部
    (2)pop:把堆栈顶部的值移除堆栈并返回这个值
    (3)top:堆栈只提供对他的顶部值得访问。top返回顶部元素的值,但它并不把顶部元素从堆栈中移除
    基本方法:当值被push到堆栈时,把他们存储到数组中连续的位置上,你必须记住最近一个被push的值得下标,如果需要执行pop操作,你只需要简单地减少这个下标值就可以了。
    额外的情况:
    (1)需要一个函数告诉我们堆栈是否为空,因为不能对一个空堆栈进行pop操作
    (2)需要另外一个函数告诉我们堆栈是否为满,因为有的堆栈存在最大长度的限制
  • 堆栈的通用实现方式的结构如下:

//一个堆栈模块的入口

#define STACK_TYPE int//堆栈所存储的值得类型

//push:把一个新值压入到堆栈中,它的参数是需要被压入的值
void push(STACK_TYPE value);

//pop:从堆栈中弹出一个值,并将其丢弃
void pop(void);

//top:返回堆栈顶部元素的值,但不对堆栈进行修改
STACK_TYPE top(void);

//is_empyt:如果堆栈为空返回TRUE,否则返回FALSE
int is_empty(void);

//is_full:如果堆栈已满,返回true,否则返回FALSE
int is_full(void);

  • 使用静态数组实现堆栈:

所有不属于外部接口的内容都被声明为static:可以防止用户使用预定义接口之外的任何方式来访问堆栈中的值

//用一个静态数组实现的堆栈,数组的长度只能通过修改#define定义,
//并对模块重新进行编译来实现

#include "stack.h"
#include <assert.h>

#define STACK_SIZE 100//堆栈中值数量的最大限制

//存储堆栈中值得数组和一个指向堆栈顶部元素的指针
static STACK_TYPE stack[STACK_SIZE];
static int top_element=-1;//top_element保存堆栈顶部元素的下标值,他的初始值为-1,提示堆栈为空

//push
void push(STACK_TYPE value)
{
assert(!is_full());
top_element+=1;//top_element始终包含顶部元素的下标值
stack[top_element]=value;
}

//pop
void pop(void)
{
assert(!is_empty());
top_element-=1;
}

//top
STACK_TYPE top(void)
{
assert(!is_empty());
return stack[top_element];
}

//is_empty
int is_empty(void)
{
return top_element==-1;
}

//is_full
int is_full(void)
{
return top_element==STACK_SIZE-1;
}

(1)这段程序使用断言调用is_full和is_empty函数而不是测试top_element本身,可以使用不同的方法来检测空堆栈和满堆栈
(2)如果static int top_element=0;也行,此时top_element将指向数组的下一个可用位置,
//push
void push(STACK_TYPE value)
{
assert(!is_full());
stack[top_element]=value;
top_element+=1;//top_element始终包含顶部元素的下标值
}

(3)传统的pop的写法,
下面操作的顺序很重要,top_element在元素被复制出数组后才减1
STACK_TYPE pop(void)
{
STACK_TYPE temp;
assert(!is_empty());
temp=stack[top_element];
top_element-=1;
return temp;
}
等价于下面的写法:
assert(!is_empty());
return stack[top_element--];

  • 使用动态数组实现堆栈
    较于静态数组,动态数组的实现是用指针代替数组,程序引入stack_size变量来保存堆栈的长度。

//下面的声明可以添加到stack.h
//creat_stack:创建堆栈,参数指定堆栈可以保存多少个元素。该函数并不用于静态数组的堆栈
void creat_stack(size_t size);//size指定数组的长度

//destry_stack:销毁堆栈,释放堆栈所使用的内存,该函数并不用于静态数组的堆栈
void destroy_stack(void);

具体实现如下:
//一个用动态分配数组实现的堆栈,堆栈的长度在创建堆栈的函数被调用的时候给出,改函数必须在任何其他操作堆栈的函数
被调用之前调用
#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;

//creat_stack
void creat_stack(size_t size)
{
assert(size==0);
stack_size=size;
stack=(STACK_TYPE *)malloc(stack_size*sizeof(STACK_TYPE));
assert(stack!=NULL);
}

//destroy_stack
void destroy_stack(void)
{
assert(stack_size>0);
stack_size=0;
free(stack);
stack=NULL;
}

//push
void push(STACK_TYPE value)
{
assert(!is_full());
top_element+=1;
stack[top_element]=value;
}

//pop
void pop()
{
assert(!is_empty);
top_element-=1;
}

//top
STACK_TYPE top(void)
{
assert(!is_empty());
return stack[top_element];
}

//is_empty
int is_empty(void)
{
assert(stack_size>0);
return top_element==-1;
}

//is_full
int is_full()
{
assert(stack_size>0);
return top_element==(stack_size-1);
}

  • 使用单链表实现链式堆栈

//思想:把一个新元素压入堆栈,是通过在链表的起始位置中添加一个元素实现的。
从堆栈中弹出一个元素是通过从链表中移除第1个元素实现的。
位于链表头部的元素总是很容易被访问。

//一个用链表实现的堆栈,这个堆栈没有长度限制
#include "stack.h"
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <assert.h>

#define FALSE 0

//定义一个结构来存储堆栈元素,其中link字段将指向堆栈的下一个元素
typedef struct STACK_NODE
{
STACK_TYPE value;
struct STACK_NODE *next;
}StackNode;

//指向堆栈中的第一个节点的指针
static StackNode *stack=NULL;//空链表

void creat_stack(size_t size) //creat_stack是一个空函数,由于链式堆栈不会填满,所以is_full函数始终会返回假
{

}

//destroy_stack
void destroy_stack(void)
{
while(!is_empty())
pop();
}

//push
void push(STACK_TYPE value)
{
StackNode *new_node;
new_node=(StackNode *)malloc(sizeof(StackNode));
assert(new_node!=NULL);
new_node->value=value;
new_node->next=stack;//这里的stack是以前的空链表
stack=new_node;//下次还要在stack前面,就是new_node前面push node啊
}

//pop
void pop()
{
StackNode *first_node;
assert(!is_empty());
first_node=stack;//从头部开始释放
stack=first_node->next;
free(first_node);
}

//top
STACK_TYPE top(void)
{
assert(!is_empty());
return stack->value;
}

//is_empty
int is_empty(void)
{
return stack==NULL;
}

//is_full
int is_full(void)
{
return FALSE;
}

3.队列

  • 队列和堆栈的顺序不同。队列是一种先进先出(First-In First-Out, FIFO)的结构。
  • 一般在队列的尾部插入以及在头部删除,因为她准确地描述了人们在排队时的实际体验。
  • 队列需要两个指针:一个指向队头,一个指向队尾。
    数组并不像堆栈那样适合队列的实现,这是由于队列使用内存的方式决定的。
  • 队列的通用实现方式的结构如下:

//一个队列模块的接口
#include <stdlib.h>
#define QUEUE_TYPE int //队列元素的类型

//creat_queue
//创建一个队列,函数指定队列可以存储的元素的最大数量
//注意:该函数只适用于使用动态分配数组的队列
void creat_queue(size_t size);

//destroy_queue
//销毁一个队列。注意:该函数只适用于链式和动态分配数组的队列
void destroy_queue(void);

//insert
//向队列添加一个元素,参数就是需要添加的元素
void insert(QUEUE_TYPE valus);

//delete
//从队列中移除一个元素并将其丢弃
void delete(void);

//first
//返回队列中第一个元素的值,但不修改队列本身
QUEUE_TYPE first(void);

//is_empty
//如果队列为空,则返回TRUE,否则返回FALSE
int is_empty(void);

//is_full
//如果队列已满,返回TRUE,否则返回FALSE
int is_full(void);

  • 用静态数组实现队列,需要注意循环数组的问题
    考虑一个用5个元素的数组实现的队列,如下所示,

    经过三次删除之后,队列的样子如下,结果:数组并未满,但是它的尾部已经没有空间,再也无法插入新的元素。

    解决办法之一是:当一个元素被删除后,队列中的其余元素朝着数组起始位置的方向移动一个位置。对于较大的队列,复制元素所需的开销太大,所以不行。
    解决办法之二:让队列的尾部环绕到数组的头部,这样新元素就可以存储到以前删除元素所流出来的空间中。该方法称之为:循环数组。 如下所示:

利用循环数组,插入一个新元素的实现:当尾部下标移出数组尾部时,把他设置为0.
rear+=1;
if (rear>=QUEUE_SIZE)
rear=0;
或者
rear=(rear+1)%QUEUE_SIZE;

在对front增值时,也必须使用同一个技巧。

使用循环数组,使得判断一个循环数组是否为空或者已满更为困难。

eg:假定队列已满,如下所示,front=3,rear=2,

(第17章)经典抽象数据类型_#include

若有4个元素从队列中删除(删除的是下标是:4,0,1,2),front将增值4次

(第17章)经典抽象数据类型_#include_02


当最后一个元素被删除时,队列中的情况如下图所示:

(第17章)经典抽象数据类型_堆栈_03

对比上述的第一张图和第三张图,可以发现:现在的front和reae的值时相同的,这和队列已满时候的情况是一样的。
即:当队列空或者满时,对front和rear进行比较,其结果都是为真。所以,我们无法通过比较front和rear来测试队列是否为空。

有两种方法解决该问题:
方法1:
引入一个新变量,用于记录队列中的元素数量。他在每次插入元素时加1,在每次删除元素时减1,。
对该变量的值进行测试就可以很容易分清队列的空间为空还是已满。


方法2:重新定义满的含义。
如果数组中的一个元素始终保留不可用,这样当队列满的时候,front和rear的值便不相同,可以和队列为空的情况区分开来。
通过不允许数组完全填满,问题便得以避免。

当队列为空时,front和rear的值应该是什么?
当队列只有一个元素的时候,我们需要使得front和rear都指向这个元素。
一次插入操作将增加rear的值,所以为了使得rear在第1次插入后,指向这个插入的元素,当队列为空时,rear的值必须比front小1.
此外,从队列中删除最后一个元素后的状态也是如此,因此,删除最后一个元素并不会造成一种特殊情况。

代码如下:
当满足下面的条件时,队列为空:
(rear+1)%QUEUE_SIZE==front

当满足下面的条件时,队列已满:因为在front和rear正好满足下面的关系之前,必须停止插入元素
(rear+2)%QUEUE_SIZE==front

  • 使用静态数组实现队列(使用不完全填满数组的的方法2)

//这种实现方式,永远不会真正填满队列,ARRAY_SIZE的值被定义为比QUEUE_SIZE大1;
//我们可以使用任何值初始化front和rear,只要rear比front小1.
//入队改变的是尾指针,出队改变的是头指针
//下面的代码始终不用的一个元素是,下标为1的元素,他用于判断队列是否为满,如下图所示:
由于满和空是重复的,所以:下面重新定义了队列满的含义:
假设数组的元素个数为4,先移动rear指针,让队列为满,再移动front指针,让队列为空

ps:因为满和空是由指针的相对位置决定的,所以,我应该可以把队列为空进行重定义。。

(第17章)经典抽象数据类型_数组_04

#include "queue.h"
#include <stdio.h>
#include <assert.h>

#define QUEUE_SIZE 100//队列中元素的最大数量
#define ARRAY_SIZE (QUEUE_SIZE+1)//数组的长度

//用于存储队列元素的数组和指向队列头和尾的指针
static QUEUE_TYPE queue[ARRAY_SIZE];
static size_t front=1;
static size_t rear=0;

//insert
void insert(QUEUE_TYPE value)
{
assert(!is_full);
rear=(rear+1)%ARRAY_SIZE;
queue[rear]=value;
}

//delete
void delete(void)
{
assert(!is_empty());
front=(front+1)%ARRAY_SIZE;
}

//first
QUEUE_TYPE first(void)
{
assert(!is_empty());
return queue[front];
}

//is_empty
int is_empty(void)
{
return (rear+1)%ARRAY_SIZE==front;
}

//is_full
int is_full(void)
{
return (rear+2)%ARRAY_SIZE == front;
}

  • 链式队列比数组形式的队列简单,不存在循环数组的问题

/需要注意的问题是:当最后一个元素被移除时,rear指针也必须被设置为NULL
//用一个链表形式实现的队列,他没有长度限制
#include "queue.h"
#include <stdio.h>
#include <assert.h>

//定义一个结构用以保存一个值。link字段将指向队列中的下一个节点。
typedef struct QUEUE_NODE
{
QUEUE_TYPE value;
struct QUEUE_NODE *next;
}QueueNode;

//指向队列第1个和最后一个节点的指针
static QueueNode *front;
static QueueNode *rear;

//destroy_queue
void destroy_queue(void)
{
while(!is_empty())
delete();
}

//insert
void insert(QUEUE_TYPE value)
{
QueueNode *new_node;

//分配一个新节点,并填充它的各个字段
new_node=(QueueNode *)malloc(QueueNode);
assert(new_node!=NULL);

new_node->value=value;
new_node->next=NULL;

//把他插入到队列的尾部
if (rear==NULL)
{
front=new_node;
}
else
{
rear->next=new_node;
}

rear=new_node;

}

//delete
void delete(void)
{
QueueNode *next_node;

//将队列的头部删除一个节点,如果他是最后一个节点,将rear也设置为NULL
assert(!is_empty());
next_node=front->next;
free(front);
front=next_node;
if(front==NULL)
rear=NULL;
}

//first
QUEUE_TYPE first(void)
{
assert(!is_empty());
return front->value;
}

//is_empty
int is_empty(void)
{
return front==NULL;
}

//is_full
int is_full()
{
return 0;
}

4.二叉搜索树

  • 树是一种数据结构,它要么为空,要么具有一个值并具有0或多个孩子child,每个孩子本身也是树
  • 二叉树binary tree是树的一种特殊形式,它的每个节点至多具有两个孩子:左孩子left和右孩子right
  • 二叉搜索树作为一种用关键值快速查找数据的优秀工具的原因是:其每个节点的值比它的左子树的所有节点都要大,但比它的右子树的所有节点的值都要小
  • eg:
    (1)二叉搜索树的每个节点具有一个双亲节点(它的上层节点),0个、1个或2个孩子(直接在他下面的节点)
    (2)根节点位于顶端:没有双亲
    (3)叶子节点位于低端:没有孩子
  • (第17章)经典抽象数据类型_数组_05

  • 在二叉搜索树中的插入
  • 从二叉搜索树中删除节点
  • 在二叉搜索树中查找
  • 树的遍历traversal
    (1)当你在检查一棵树的所有节点时,你就在遍历这颗树
    (2)遍历树的方法有:前序pre-order,中序in-order,后序post-order,层次遍历breadth-first。 所有类型的遍历都是从树的根节点或子树的根节点开始。
    (3)eg:图17.1的二叉搜索树的遍历结果是:

前序遍历:先检查所有节点的值,然后递归遍历左子树和右子树
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

  • 二叉搜索树的通用接口写法
    下面的接口提供了用于把值插入到一颗二叉搜索树的函数的原型

//二叉搜索树模块的接口
#define TREE_TYPE int //数的值类型

//insert:向树添加一个新值,参数是需要被添加的值,它必须原先并不存在于树中
void insert(TREE_TYPE value);

//find:查找一个特定的值,这个值作为第1个参数传递给函数
TREE_TYPE *find(TREE_TYPE value);

//pre_order_traverse
执行数的前序遍历,它的参数是一个回调函数的指针,它所指向的函数将在数中处理每个节点被调用,节点的值作为参数传递给这个函数
void pre_order_traverse(void (*callback)(TREE_TYPE value));

  • 数组形式的二叉搜索树

(1)用数组表示树的关键是使用下标来寻找某个特定值的双亲和孩子
若数组的下标从1开始:
节点N的双亲是节点N/2
节点N的左孩子是节点2N
节点N的右孩子是节点2N+1

若数组的下标是从0开始的:
节点N的双亲是节点(N+1)/2-1
节点N的左孩子是节点2N+1
节点N的右孩子是节点2N+2

用方式(1)来实现
//一个用静态数组实现的二叉搜索树,数组的长度只能通过修改#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;
}

//insert
void insert(TREE_TYPE value)
{
int current;

//确保值为非零,因为零用于提示一个未使用的节点

assert(value!=0);

//从根节点开始
current=1;

//从合适的子树开始,直到到达一个叶节点
while(tree[current]!=0)
{
//依据情况,进入叶节点或者右子树(确信未出现重复的值)
if (value<tree[current])
current=left_child(value);//比较下标
else
{
assert(value!=tree[current]);
current=right_child(current);//比较下标
}
assert(current<ARRAY_SIZE);
}
}

//find
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]);//回调TREE_TYPE * find(TREE_TYPE value)
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);
}

  • 链式二叉搜索树
    (1)为每个新值动态分配内存,并把这些结构链接到树中
    (2)树的每个节点用一个结构来容纳值和两个指针,该指针最初为NULL,表示为一颗空树。

//一个使用动态分配的链式结构实现的二叉搜索树
#include "tree.h"
#include <assert.h>
#include <stdio.h>
#include <malloc.h>

//TreeNode结构包含了值和两个指向某个树节点的指针
typedef struct TREE_NODE
{
TREE_TYPE value;
struct TREE_NODE *left;
struct TREE_NODE *right;
}TreeNode;

//指向树根节点的指针
static TreeNode *tree;

//insert
void insert(TREE_TYPE value)
{
/*
current指针用于检查树中的节点,寻找新值插入的合适位置;
link指针指向另一个节点,link字段指向当前正在检查的节点。当到达一个叶节点时,该指针必须进行修改以插入新节点。
该函数自上而下,根据新值和当前节点值的比较结果,选择进入左子树或者右子树,直到到达叶节点。
然后,创建一个新节点并链接到树中,该迭代算法在插入第1个节点时,也能够正确处理,不会造成特殊情况。
*/
TreeNode *current;
TreeNode **link;

//从根节点开始
link=&tree;

//持续查找值,进入合适的子树
while((current=*link)!=NULL)
{
//依据情况。进入左子树或右子树(确认没有出现重复的值)
if (value < current->value)
link=&(current->left);
else
{
assert(value!=current->value);
link=&(current->right);
}
}

//分配一个新节点,使适当节点的link字段指向它
current=malloc(sizeof(Treenode));
assert(current!=NULL);
current->value=value;
current->left=NULL;
current->right=NULL;
*link=current;

}

//find
//find函数可以返回一个指向这个树节点的指针而不是节点值,可以允许用户利用该指针执行其他形式的遍历
TREE_TYPE *find(TREE_TYPE value)
{
TreeNode *current;
//从根节点开始,直到找到该值,进入合适的子树
current=tree;
while (current!=NULL && current->value!=value)
{
//依据情况,进入左子树或右子树
if (value<current->value)
current=current->left;
else
current=current->right;
}

if (current !=NULL)
return &(current->value);
else
return NULL;

//执行一层前序遍历,该函数用于保存我们当前正在处理的节点的信息
//该函数并不是用户接口的一部分

static void do_pre_order_traverse(TreeNode *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);
}


举报

相关推荐

0 条评论