西风的数据结构教程(1)——链表

今天,同学熊问了我一些基本数据结构的问题,我想这些基础的东西还是应该好好理解的。其实数据结构应该是计算机技术的基石,各种算法都是在数据管理的基础上运行的。

于是,我打算利用业余时间,将我学过的这部分内容整理出来,并且加上一些自己的创新性的内容,做成一份简明扼要的数据结构教程,然后尽可能的深入探讨一些关于这些内容的创新方法和优雅的实现。

回想当年,高中时期的计算机竞赛生涯,一遍遍的啃那些不懂的知识点,也正是因为如此,我虽不算出色,但也拥有了扎实的基本功,在此,我也要感谢我的恩师,我们唐山一中的郭莲凤老师。

恩,那么我将梳理整个数据结构和相关重要算法的脉络,希望由浅入深,逐渐带大家了解各种有趣的问题和精妙的解法,领略数据之美。

本文代码经过基本测试验证,但还是请避免不经测试的使用到生产环境,本人才疏学浅,如果存在疏漏,还望广大网友批评指正。

数据结构简介

数据结构的基础是数据在计算机中的存储结构,最基础的是数组、链表这两种结构。

图1 数组和链表

由链表的思想构建出来的,就是树结构,树结构能非常方便的实现各种快速查询、动态数据管理等难题。
平衡二叉树,应该是最常用的树状结构,用来做TreeMap,用来实现集合Set,可以实现自动排序和判重。

类似树的结构就是图,邻接表就是链表思想的产物,但图还可以按照矩阵的方式存储。
图就有联通分量、图搜索、最短路径、网络流等经典问题。

链表简介

今天我们先介绍链表,因为数组显而易见,连续存放,计算机较为容易实现。

链表的一大好处就是不用确定空间长度,不够的时候,再申请新的节点,而且插入十分方便。

图2,数组不能动态的插入,而链表支持随意插入和删除

决定数据结构的这样的性质的原因是计算机内存是抽象的连续空间,假若计算机的内存本身就不是这样组织的,也许就没有链表或线性表这种常用的结构了。

回到正题,链表实现的实际上就是计算机空间的一种动态管理,指针跳转的思路也是计算机中管理数据的精髓,我们下面就看一下计算机中指针到底是个什么样子。

指针是一个内存地址的记录,一般用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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值