今天,同学熊问了我一些基本数据结构的问题,我想这些基础的东西还是应该好好理解的。其实数据结构应该是计算机技术的基石,各种算法都是在数据管理的基础上运行的。
于是,我打算利用业余时间,将我学过的这部分内容整理出来,并且加上一些自己的创新性的内容,做成一份简明扼要的数据结构教程,然后尽可能的深入探讨一些关于这些内容的创新方法和优雅的实现。
回想当年,高中时期的计算机竞赛生涯,一遍遍的啃那些不懂的知识点,也正是因为如此,我虽不算出色,但也拥有了扎实的基本功,在此,我也要感谢我的恩师,我们唐山一中的郭莲凤老师。
恩,那么我将梳理整个数据结构和相关重要算法的脉络,希望由浅入深,逐渐带大家了解各种有趣的问题和精妙的解法,领略数据之美。
本文代码经过基本测试验证,但还是请避免不经测试的使用到生产环境,本人才疏学浅,如果存在疏漏,还望广大网友批评指正。
数据结构简介
数据结构的基础是数据在计算机中的存储结构,最基础的是数组、链表这两种结构。
由链表的思想构建出来的,就是树结构,树结构能非常方便的实现各种快速查询、动态数据管理等难题。
平衡二叉树,应该是最常用的树状结构,用来做TreeMap,用来实现集合Set,可以实现自动排序和判重。
类似树的结构就是图,邻接表就是链表思想的产物,但图还可以按照矩阵的方式存储。
图就有联通分量、图搜索、最短路径、网络流等经典问题。
链表简介
今天我们先介绍链表,因为数组显而易见,连续存放,计算机较为容易实现。
链表的一大好处就是不用确定空间长度,不够的时候,再申请新的节点,而且插入十分方便。
决定数据结构的这样的性质的原因是计算机内存是抽象的连续空间,假若计算机的内存本身就不是这样组织的,也许就没有链表或线性表这种常用的结构了。
回到正题,链表实现的实际上就是计算机空间的一种动态管理,指针跳转的思路也是计算机中管理数据的精髓,我们下面就看一下计算机中指针到底是个什么样子。
指针是一个内存地址的记录,一般用16进制数字表示,32位或64位机器,就是指其用来寻址的总线的位数,我们记录一下数据在计算机内具体存放在哪里,然后就可以在任意时刻找到这个数据。
链表的实现
为了让链表遍历和插入的时候更加方便,我们给链表多增加一个头节点,因为我们希望便利的描述在任意位置插入一个数据,如果链表第一个节点存储第一个数据,定义插入操作是在节点的后面插入,那么我们将不好定义向第一个节点头部插入数据的操作。
那么我们来写一个list.h
文件,用C代码编写链表的实现:
#ifndef LIST_H
#define LIST_H
#endif // LIST_H
C语言中,这种定义方式是为了防止头文件的重复引用,由于默认是C语言,我们就不添加 extern "C"
标识了。
#ifndef LIST_H
#define LIST_H
typedef int ListElementType;
typedef struct _list
{
ListElementType data;
struct _list* next;
} list;
#endif // LIST_H
这里是链表的经典定义方式,想必学过C数据结构的同志们也十分熟悉,这里介绍一下为何要用ListElementType
单独定义类型,C代码不支持模板,代码的定义应该尽可能的灵活,C中的类型定义对提高代码的复用和可移植性上具有重要意义。将类型重定义,往往能让同样的代码运行在不同的环境下。
经典的size_t
变量,就是为了让C代码在32位平台上和64位平台上,返回的分别是32位整数和64位整数,他往往被用来描述内存长度,其规模当然要和平台相适应。
typedef struct _list
{
} list;
这种定义方式也是C独特的,因为C中的struct类型在使用时必须这样struct typename idname
, 这样定义一下,就方便了使用。
下面我们添加一些操作函数:
/* 链表末尾的添加,返回新添加的节点 */
list* ListAdd(list* l, ListElementType data);
/* 链表任意位置的添加,会添加到l节点的后面,返回新添加的节点 */
list* ListInsert(list* l, ListElementType data);
/* 删除链表,l是指要删除的节点的上一个节点 */
void ListDelete(list* l, list* ele);
/* 判定是否是空 */
bool ListIsEmpty(list* l);
/* 创建链表节点 */
list* ListCreate();
链表节点的创建很简单,仿照C++,编写一个构造函数,为节点动态分配内存,并把节点的next指针置为NULL:
list* ListCreate() {
list* pList = (list*) malloc(sizeof(list));
pList->next = NULL;
return pList;
}
判断一个链表是否为空十分方便,空链表只有一个头节点,并且头节点不保存数据,于是我们只需要让ListIsEmpty
函数接受一个链表的头节点作为输入即可:
bool ListIsEmpty(list* l) {
return l->next == NULL;
}
这里我们并没有判断l十分是NULL, 我们可用检查,但即使发现了,但也不好处理,所以为了简单,我们索性要求用户必须提供给我们一个非空的头节点,这也是仿照C++中的对象的概念,我们在调用对象的成员函数时,成员函数也一般不去判断this指针是否为空。
链表的插入函数往往是最重要的,在任意位置插入是链表的核心功能,不过也很简单,只要打断链表,再插入即可:
list* ListInsert(list* l, ListElementType data) {
list* oldnext = l->next;
l->next = ListCreate();
l->next->data = data;
l->next->next = oldnext;
return l->next;
}
任意位置的删除也十分重要,链表的内存管理一定要注意,时刻避免内存泄露:
void ListDelete(list* l, list* ele) {
list* pList = l;
while(pList->next != NULL) {
if (pList->next == ele) {
pList->next = ele->next;
Free(ele);
break;
}
pList = pList->next;
}
}
为了方便用户的数据添加,编写一个添加函数,将节点添加到链表的末尾:
list* ListAdd(list* l, ListElementType data) {
list* pList = l;
while(pList->next != NULL) {
pList = pList->next;
}
return ListInsert(pList, data);
}
但这个函数每次调用都会遍历整个链表,效率不高,需要注意。
C语言的一个重要的特点就是用宏简化编程,我们为了方便链表的遍历,我们编写一个宏来遍历:
/* 链表的遍历宏 */
#ifndef list_for_each
#define list_for_each(type, ele, list) \
{ type ele; for (ele = list->next; ele != NULL; ele = ele->next) {
#endif
#ifndef end
#define end } }
#endif
C语言的宏功能很强大,可以将任意文本段的编译时填入到对应的位置,我们这样写好一个宏后,用户就可以这样调用:
list_for_each(list*, ele, l)
printf("%d\n", ele->data);
end
是不是有高级语言的foreach的效果了?
至此,链表的基本写法已经结束完了,在附录中有本文完整代码,需要的朋友可以参考。
链表的高级用法
下面我讨论一下链表的变种,这些链表往往很具有实用价值
双链表
双链表的诞生是为了解决链表节点不知道自己前面一个节点的尴尬,于是在节点的定义中,存在一个前驱,一个后继,这样往往能找到上一个节点,去处理一下相关的事情。
双循环链表
双循环链表在Linux内核中拥有广泛的应用,往往被当做简单的容器实用,其遍历方便,任意一个位置都可以开始向前或向后遍历,并且都能遍历完全部元素。
块状链表
数组其实也是一种很重要的结构,但其不易修改,但有的时候,我们非常需要数组连续随机存储的特性,但同时又要经常修改,如果一般只是从两端修改的话,可以采取块状链表的解决方案:
如图所示,块状链表是由链表串起来的数组,前面后面添加数据无需修改大量内容,只需要增删节点即可,而且又能较快的索引到想要的数据,C++STL中的deque就是这种经典的结构。
附录——完整代码
/* list.h */
/*
* @Author: sxf
* @Date: 2015-04-14 19:44:32
* @Last Modified by: sxf
* @Last Modified time: 2015-04-14 21:04:24
*/
#ifndef LIST_H
#define LIST_H
#include <malloc.h>
typedef char bool;
typedef int ListElementType;
typedef struct _list
{
ListElementType data;
struct _list* next;
} list;
/* 链表末尾的添加,返回新添加的节点 */
list* ListAdd(list* l, ListElementType data);
/* 链表任意位置的添加,会添加到l节点的后面,返回新添加的节点 */
list* ListInsert(list* l, ListElementType data);
/* 删除链表,l是指要删除的节点的上一个节点 */
void ListDelete(list* l, list* ele);
/* 判定是否是空 */
bool ListIsEmpty(list* l);
/* 创建链表节点 */
list* ListCreate();
/* 链表的遍历宏 */
#ifndef list_for_each
#define list_for_each(type, ele, list) \
{ type ele; for (ele = list->next; ele != NULL; ele = ele->next) {
#endif
#ifndef end
#define end } }
#endif
/* 指针的释放宏 */
#ifndef Free
#define Free(p) if (p!=NULL) free(p)
#endif
list* ListAdd(list* l, ListElementType data) {
list* pList = l;
while(pList->next != NULL) {
pList = pList->next;
}
return ListInsert(pList, data);
}
list* ListInsert(list* l, ListElementType data) {
list* oldnext = l->next;
l->next = ListCreate();
l->next->data = data;
l->next->next = oldnext;
return l->next;
}
void ListDelete(list* l, list* ele) {
list* pList = l;
while(pList->next != NULL) {
if (pList->next == ele) {
pList->next = ele->next;
Free(ele);
break;
}
pList = pList->next;
}
}
bool ListIsEmpty(list* l) {
return l->next == NULL;
}
list* ListCreate() {
list* pList = (list*) malloc(sizeof(list));
pList->next = NULL;
return pList;
}
#endif // LIST_H
/* main.c */
/*
* @Author: sxf
* @Date: 2015-04-14 19:44:24
* @Last Modified by: sxf
* @Last Modified time: 2015-04-14 21:20:00
*/
#include <stdio.h>
#include "list.h"
list* l = NULL;
list* last = NULL;
int main() {
l = ListCreate();
/* 低效率的添加 */
printf("test1:\n");
ListAdd(l, 3);
ListAdd(l, 5);
last = ListAdd(l, 8);
list_for_each(list*, ele, l)
printf("%d\n", ele->data);
end
/* 推荐的添加 */
printf("test2:\n");
last = ListInsert(last, 2);
last = ListInsert(last, 3);
last = ListInsert(last, 5);
list_for_each(list*, ele, l)
printf("%d\n", ele->data);
end
/* 删除的方式 */
printf("test_del:\n");
list_for_each(list*, ele, l)
if (ele->data == 2) {
ListDelete(l, ele);
break; /* 一般循环中删除必须打断循环 */
}
end
list_for_each(list*, ele, l)
printf("%d\n", ele->data);
end
return 0;
}