c语言算法高级,C语言简明教程(十九):高级数据结构和算法详解

数据结构和算法是C语言的主要内容,更特别在于C语言的数据结构和算法一般需要自己实现,与OOP语言不同,C标准库中没有提供相关的数据结构。Linux C标准库原为Linux Libc,现在常用的是GLibc,即GNU C Library,另外可用的库还有GTK的库Glib,POSIX标准库Gnulib,其中Glib中提供有完整的数据结构和相关算法操作。本文详细讨论C数据结构的标准定义及其算法实现,自定义实现的一个好处是时数据表示更加灵活,前提是数据结构和算法的设计要标准,通常自定义结合其它库开发会更有效率。

一、抽象数据类型(ADT)

抽象数据类型是基本数据类型复合而来的类型,基本数据类型包括数据的存储方式以及操作方式(运算符),抽象数据类型同样也有,而且这是抽象数据类型的最基本表示形式,这里先讨论ADT的数据结构和算法操作的标准定义形式,其它例如链表、队列、二叉树、栈等都可以使用这些标准形式。

抽象数据类型在外部应以容器看待,所以核心的问题就变成容器的设计了。

1、基本信息

抽象数据类型一般使用一个结构体表示,最基本需要有数据属性,即结构体成员数据。

算法操作类似运算符,算法操作是数据自身的运算规则,操作对象包括:成员数据和结构体对象本身。常见的操作有两种,一种是很直接的从需求中抽象而来,这种需要按情况而定。

另一种则是标准操作,详细有一般数据操作:增加、删除、修改、查找和排序;生命周期操作:初始化、释放(清空);高级操作:在任意位置插入项、移除任意位置的项、替换项、容器的项个数、判断容器是否为空、判断容器是否已满(或判断空间是否足够)。

2、创建接口(.h)

(1)数据存储方式

在这里我们的目的是设计一个容器,如果你用过OOP中的容器那会容易理解一些,主要包括容器本身以及一般的业务数据对象,首先是一般的业务数据,这种数据对象是直接的需求数据,也就是不是固定的,不同需求有不同的表现形式,一般都是使用结构体定义,如下代码:

// 一般数据[自定义]:一般的业务数据,不同需求定义不同的数据形式

struct food{ // 食品

char name[256]; // 名称

float price; // 价格

};

typedef struct food Item;

以上定义是一般性的,不是固定的定义,下面介绍的具有普遍性,面向容器设计,首先需要定义容器中的数据单元,在C语言中这个数据单元又可称为抽象结点Node,使用一个结构体定义,这个抽象结点用于保存上面的业务数据Item,特定数据结构的主要表示工具(例如链表和二叉树在这里会有不同的形式),下面以链表举例,如下代码:

// 抽象结点:数据的基本单元

struct node{

Item *item; // 可用void *表示存储任意类型的数据对象

struct node *next; // 下一个数据单元的地址

};

typedef struct node Node;

可能会有人将Node和Item合并来写,也是可以的,或者会有人写到这步就要开始创建接口并实现了,然后将头指针和容器大小变量直接写在外部了,麻烦有很多,这里采取一种简单的方式,使用一个结构体封装头指针等这些必要信息,这样我们只需要这个结构体就可以操作容器中的所有元素了,可以说这个结构体表示了整个抽象数据类型(如链表)的接口了,类似于OOP中的类,数据成员主要是特定数据结构必要的一些全局信息,比如链表需要的头指针、尾指针和项数,定义形式如下:

// 标准数据类型接口:代表特定数据结构的所有数据,一种抽象数据类型模板

struct linkedlist{

Node *head;

Node *tail;

unsigned int size;

};

typedef struct linkedlist LinkedList;

一般数据、数据单元和标准接口,以上的表示形式是数据结构和算法的一个重点内容,特别是标准接口,如果没有这个那么操作就不会那么理想了。另外一个难点内容就是算法实现,可能会用到解决方式是while循环和递归调用,其中递归调用不推荐使用,一来是理解困难,二来内存使用过大,而while循环也容易看明白,而且执行完一次循环后会释放掉空间。

(2)算法操作

下面是算法操作函数的定义,其中要注意const和restrict关键字的使用,const表示传入的数据不允许修改,而restrict表示该指针是访问数据对象唯一且最初的方式。另外函数声明时推荐写好函数注释,注释至少包括:函数功能说明、函数参数说明和函数返回值说明。首先说明,有一个初始化的函数接口一般都是有的,完整声明如下:

/**

* @description 初始化链表为空

* @param linkedList 链表指针,需要已分配内存空间

* @return 返回空

* */

extern void list_init(LinkedList * restrict list);

/**

* @description 添加一个元素到链表中

* @param list 链表指针

* @param item 数据单元Food

* @return true:添加成功,false: 添加失败

* */

extern bool list_add(LinkedList *list, Item *item);

/**

* @description 获取链表中指定名称的第一个元素

* @param list 链表指针

* @param name Food的名称

* @return 获取到的Food,找不到返回NULL

* */

extern Item* list_get_by_name(const LinkedList *list, char *name);

/**

* @description 清空链表

* @param list 链表指针

* @return 返回空

* */

extern void list_clear(LinkedList * list);

(3)实现接口(.c)

在实现文件中如果还需要额外的函数支持,可使用static声明,类似于OOP中的private,完整实现代码如下:

void list_init(LinkedList *list){

list->size = 0;

list->head = NULL;

list->tail = NULL;

}

bool list_add(LinkedList *list, Item *item){

Node *node = (Node *)malloc(sizeof(Node));

Item *new_item = (Item *)malloc(sizeof(Item));

strcpy(new_item->name, item->name);

new_item->price = item->price;

if(!node)

return -1;

node->item = new_item;

node->next = NULL;

if(list->head == NULL){

list->head = node;

list->tail = node;

list->size++;

}

else{

list->tail->next = node;

list->tail = node;

list->size++;

}

return 1;

}

Item* list_get_by_name(const LinkedList *list, char *name){

Node *node = list->head;

while (node != NULL){

if(strcmp(name, node->item->name) == 0)

return node->item;

node = node->next;

}

return NULL;

}

void list_clear(LinkedList * list){

Node *node = list->head;

while (node != NULL){

Node *temp = node->next;

free(node);

node = temp;

}

list->size = 0;

list->head = NULL;

list->tail = NULL;

free(list);

}

(4)使用接口

对于接口设计得好不好,主要体现在使用上,最好的方式莫过于OOP那样的调用了,在这里list_init这个函数可以改进,可以增加让该函数负责创建list,这样就不用再外部malloc了,然后让list_init函数返回相应的指针,下面是接口使用的代码:

// 初始化容器

LinkedList *list = (LinkedList *)malloc(sizeof(LinkedList));

list_init(list);

Item item1;

strcpy(item1.name, "Apple");

item1.price = 12.6;

Item item2;

strcpy(item2.name, "Pear");

item2.price = 30.94;

Item item3;

strcpy(item3.name, "Meat");

item3.price = 28.39;

// 添加数据到容器中

list_add(list, &item1);

list_add(list, &item2);

list_add(list, &item3);

// 在容器中查找数据

Item *pitem = list_get_by_name(list, "Meat");

if(pitem){

printf("name: %s\n", pitem->name);

printf("price: %.2f\n", pitem->price);

}

// 记得释放容器中的数据

list_clear(list);

二、数组和动态数组

数组一般是一次性分配足够的空间,连续存储数据,可以随机访问数据,最大的确定就是插入和删除元素费时,需要频繁地移动其它数据项,但数组适用于经常性查找的场景中。对于数组查找,一般来说有两种方式,顺序查找,可先排序再查找,另一种是二分法查找,同样需要结合排序,快速排序可使用qsort。

一般的数组是存储在栈内存中,这类数组长度固定,而且内存自由度不大。动态数组则是在堆中储存,同时长度可变长,这里以动态数组为主,数组的一般内存结构如下图:

48c2b6b992d3b13fb022573a06cd2974.png

1、数据接口表示

数据接口表示包括数据表示、算法操作,在实际使用中即使是动态数组仍然不能简单地使用malloc进行实现,仍然需要按照标准形式,一般数据、数据单元和抽象接口,完整代码如下:

// 一般数据类型:日志信息

typedef struct log{

char *date;

char message[256];

} Log;

// 数据单元:已分配内存数组的一个单元,因没有额外的信息,可直接写在标准接口中

// 标准接口

typedef struct arraylist{

unsigned int dfsize; // 数组默认初始化大小,若不指定默认为10

unsigned int size; // 数组长度

Log *array; // 数组指针

} ArrayList;

/**

* @description 创建并初始化数组容器

* @param dfsize 默认数组初始化大小

* @return 新创建的数组容器

* */

extern ArrayList* alist_init(unsigned int dfsize);

/**

* @description 添加元素到数组中

* @param list 数组指针

* @param log 数据元素指针

* @return 添加成功返回true,失败返回false

* */

extern bool alist_add(ArrayList *list, Log *log);

/**

* @description 将数组的每个元素都执行同一个函数plog

* @param list 数组指针

* @param plog 函数指针

* @return 返回空

* */

extern void alist_logs(const ArrayList *list, void (*plog)(Log *));

/**

* @description 清空数组,不能再使用list访问数据

* @param list 数组指针

* @return 返回空

* */

extern void alist_clear(ArrayList *list);

2、接口实现

ArrayList* alist_init(unsigned int dfsize){

if(dfsize < 1)

dfsize = 2;

ArrayList *list = (ArrayList*)malloc(sizeof(ArrayList));

list->dfsize = dfsize;

list->size = 0;

Log *array = (Log*)malloc(dfsize * sizeof(Log));

list->array = array;

return list;

}

bool alist_add(ArrayList *list, Log *log){

if(list->size >= list->dfsize){

list->dfsize += 2;

list->array = (Log*)realloc(list->array, list->dfsize * sizeof(Log));

}

Log *ele = list->array + list->size;

ele->date = log->date;

strcpy(ele->message, log->message);

list->size++;

return true;

}

void alist_logs(const ArrayList *list, void (*plog)(Log *)){

for (int i = 0; i < list->size; ++i) {

plog(list->array + i);

}

}

void alist_clear(ArrayList *list){

free(list->array);

list->array = NULL;

list->size = 0;

list->dfsize = 10;

free(list);

}

3、使用接口

ArrayList *arrayList = alist_init(0);

Log log1;

log1.date = local_date();

strcpy(log1.message, "AppleWebkit Chrome Mac");

Log log2;

log2.date = local_date();

strcpy(log2.message, "Ubuntu Linux Safari");

Log log3;

log3.date = local_date();

strcpy(log3.message, "Webkit Windows Chrome");

alist_add(arrayList, &log1);

alist_add(arrayList, &log2);

alist_add(arrayList, &log3);

alist_logs(arrayList, logs);

alist_clear(arrayList);

三、链表

链表的具体示例在上面介绍抽象数据类型(ADT)中,其主要特点是,一次分配一个数据空间,在堆中分散存储数据,只能进行顺序访问,不能随机访问,而且也不方便查找,只能使用顺序查找,所以链表不适用于经常性查找的场景,但是使用链表可以快速插入和删除数据,所以如果不是经常性遍历的情况都可以使用链表,链表的一般形式如下图:

c040688be431facd0df863550421ebf3.png

四、队列

队列抽象数据结构来自排队,排队的人先入队先出队,队列同样是先进先出(FIFO),队列的主要特点是从尾部添加数据,从头部移除数据。实现方式可以使用数组和链表,但是使用数组有一个问题,例如移除头部元素需要移动其它所有元素,效率不高,这里使用链表实现,队列的一般形式如下:

dde955214322ddd79f90b72edec2d498.png

实现队列抽象类型同样只需三个:一般数据、队列数据单元、队列抽象标准数据类型接口,至于队列的应用要看情况,队列一般不会去直接操作中间的元素,而只操作前后的元素。

1、数据表示和算法操作

// 一般数据表示:饮料

typedef struct drink{

char name[128];

float price;

} Drink;

// 队列基本数据单元

typedef struct element{

Drink *drink;

struct element *next;

} Element;

// 抽象接口

typedef struct queue{

Element *head;

Element *tail;

unsigned int size;

} Queue;

extern Queue* queue_init();

extern bool queue_push(Queue *queue, Drink *drink);

extern Drink* queue_pop(Queue *queue);

extern void queue_print(Queue *queue);

extern void queue_clear(Queue *queue);

2、接口实现

Queue* queue_init(){

Queue *queue = (Queue*)malloc(sizeof(Queue));

queue->size = 0;

queue->head = NULL;

queue->tail = NULL;

return queue;

}

bool queue_push(Queue *queue, Drink *drink){

Element *element = (Element*)malloc(sizeof(Element));

Drink *dk = (Drink*)malloc(sizeof(Drink));

if(!element || !dk)

return false;

dk->price = drink->price;

strcpy(dk->name, drink->name);

element->drink = dk;

element->next = NULL;

if(!queue->head){

queue->head = element;

queue->tail = element;

queue->size++;

}

else{

queue->tail->next = element;

queue->tail = element;

queue->size++;

}

return true;

}

Drink* queue_pop(Queue *queue){

Element *element = queue->head;

if(!element)

return NULL;

queue->head = element->next;

return element->drink;

}

void queue_print(Queue *queue){

Element *element = queue->head;

while (element){

printf("%8s %.3f\n", element->drink->name, element->drink->price);

element = element->next;

}

}

void queue_clear(Queue *queue){

Element *element = queue->head;

while (element){

Element *temp = element->next;

free(element);

element = temp;

}

}

3、队列接口使用

Queue *queue = queue_init();

Drink d1;

strcpy(d1.name, "Red Wine");

d1.price = 687.99;

Drink d2;

strcpy(d2.name, "Cola");

d2.price = 18.55;

Drink d3;

strcpy(d3.name, "Water");

d3.price = 2.0;

queue_push(queue, &d1);

queue_push(queue, &d2);

queue_push(queue, &d3);

queue_print(queue);

queue_clear(queue);

五、二叉树

二叉树比以上介绍过的难好多,二叉树的基本形式是:数据项、左结点和右结点指针,每个根结点都有两个子结点,第一个根结点没有父结点,如下图:

e86a31cd52925c57fc8653212033598d.png

每个根结点的左右结点又可称为左右儿子,要表示二叉树数据结构并不困难,困难的地方在于如何添加和遍历数据,有两种方式:while循环和递归调用,这两种都是基于根结点的概念,我们可以将一个根结点以及两个子结点看作一个单元。

另外,二叉树的实现原理是基于二分查找进行排序,例如将新添加的项和根结点的数据项作对比,小于在左结点大于在右结点。

二叉树相比数组和链表,同时适用经常性查找,以及频繁插入和删除,二叉树的数据表示形式同样是围绕一般数据、单元数据项和抽象数据接口。

1、数据结构和算法声明

// 一般数据:商品

typedef struct good{

unsigned int id;

char title[256];

float price;

} Good;

// 数据单元结点

typedef struct cell{

Good *good;

struct cell *left;

struct cell *right;

} Cell;

// 二叉树主要接口

typedef struct tree{

Cell *root;

unsigned int size;

} Tree;

extern Tree* tree_init();

extern bool tree_add(Tree *tree, Good *good);

extern bool tree_delete_by_id(Tree *tree, unsigned int id);

extern Good* tree_get_by_id(Tree *tree, unsigned int id);

extern void tree_clear(Tree *tree);

2、接口实现

在这里暂且不实现删除元素的操作,下次详细讨论数据结构和算法的时候再介绍清楚,因为涉及的算法比较复杂,下面是部分实现代码:

#include "tree.h"

#include

#include

Tree* tree_init(){

Tree *tree = (Tree*)malloc(sizeof(Tree));

tree->size = 0;

tree->root = NULL;

return tree;

}

bool tree_add(Tree *tree, Good *good){

Cell *cell = (Cell*)malloc(sizeof(Cell));

Good *gd = (Good*)malloc(sizeof(Good));

if(!cell || !gd)

return false;

gd->id = good->id;

strcpy(gd->title, good->title);

gd->price = good->price;

cell->good = gd;

cell->left = NULL;

cell->right = NULL;

if(!tree->root){

tree->root = cell;

tree->size++;

return true;

}

Cell *root = tree->root;

while (root != NULL){

if(root->good->id >= good->id){ // 先确定前后(左子树在根结点的前面,右子树在根结点的后面)

if(root->left == NULL){ // 再确定左右

root->left = cell;

break;

}

else{

root = root->left;

}

}

else{

if(root->right == NULL){

root->right = cell;

break;

}

else{

root = root->right;

}

}

}

return true;

}

bool tree_delete_by_id(Tree *tree, unsigned int id){

return false;

}

Good* tree_get_by_id(Tree *tree, unsigned int id){

Cell *root = tree->root;

while(root != NULL){

if(root->good->id == id){

return root->good;

}

else{

if(root->good->id > id){

root = root->left;

}

else{

root = root->right;

}

}

}

return NULL;

}

void tree_clear(Tree *tree){

}

3、二叉树接口使用

Tree *tree = tree_init();

Good g1;

g1.id = 8;

strcpy(g1.title, "Apple");

g1.price = 19.99;

Good g2;

g2.id = 18;

strcpy(g2.title, "Pure");

g2.price = 20.79;

Good g3;

g3.id = 7;

strcpy(g3.title, "Mapple");

g3.price = 60.37;

tree_add(tree, &g1);

tree_add(tree, &g2);

tree_add(tree, &g3);

Good *gp = tree_get_by_id(tree, 7);

printf("%d\n%s\n%.2f\n", gp->id, gp->title, gp->price);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值