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

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

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

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

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

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

数据结构简介

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

由链表的思想构建出来的,就是树结构,树结构能非常方便的实现各种快速查询、动态数据管理等难题。

平衡二叉树,应该是最常用的树状结构,用来做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;
}
时间: 2024-10-11 22:19:47

西风的数据结构教程(1)——链表的相关文章

西风的数据结构教程(2)——队列

队列是一种简单的先进先出结构,各种需要排队的事情,都可以开一个队列来完成. 利用链表或数组,都能实现队列,不过最大的区别就是,数组的扩展比较困难,而链表较为容易,但链表资源消耗稍多. 数据结构的不同导致了队列的实现也不相同,链表上次已经实现过了,只需简单包装即可使用,这里,我们介绍简单的用数组模拟队列的方式: 这个队列是固定长度的一个数组构建的,另外保存两个int数字,负责记录数组的下标索引. 我们下面就来编写一下这个队列,还是使用C语言,在此,我会继续介绍C语言的基础知识. 队列实现 复习一下

I学霸官方免费教程三十七:Java数据结构之单向链表结构

数据结构之单向链表 例如:现有双向链表OneWayLinked中存储着1,2,3,4四个元素,那么集合对象中会有4个节点A.B.C.D,由上述结构可以知道,节点A中存储着元素1和节点B:节点B中存储着元素2和节点C,节点C中存储着元素3和节点D,节点D中存储着元素4和null.如果现在要在元素2和3中间插入一个元素5:过程如下:1.创建节点E,E中存储元素52.将B中的下一个节点修改为节点E3.将E中的下一个节点赋值为节点C从上述过程看,插入时没有节点位置移动的操作,所以效率比较高:删除的过程和

数据结构线性表链表的C语言实现

                                                                                      数据结构线性表链表的C语言实现      说明:线性表是一种最简单的线性结构,也是最基本的一种线性结构,所以它不仅是学习中的重点,也是应用开发非常常用的一种数据结构.它可以分为顺序表和链表.它的主要操作是数据元素的插入,删除,以及排序等.接下来,本篇文章将对线性表链表的基本操作和运用进行详细的说明(包含在源代码的注释中),并给

C#数据结构-单链表

理论基础: 链表是用一组任意的存储单元来存储线性表中的数据元素. 如果结点的引用域只存储该结点直接后继结点的存储地址,则该链表叫单链表(Singly Linked List). 单链表由头引用H唯一确定.头引用指向单链表的第一个结点,也就是把单链表第一个结点的地址放在H中. C#实现: 1接口 引用线性表的接口IListDS<T> 2实现 首先,必须定义一个单链表的节点类.  1 public class Node<T> 2    { 3        private T data

数据结构实验之链表五:单链表的拆分

数据结构实验之链表五:单链表的拆分 Time Limit: 1000MS Memory limit: 65536K 题目描述 输入N个整数顺序建立一个单链表,将该单链表拆分成两个子链表,第一个子链表存放了所有的偶数,第二个子链表存放了所有的奇数.两个子链表中数据的相对次序与原链表一致. 输入 第一行输入整数N;: 第二行依次输入N个整数. 输出 第一行分别输出偶数链表与奇数链表的元素个数: 第二行依次输出偶数子链表的所有数据: 第三行依次输出奇数子链表的所有数据. 示例输入 10 1 3 22

数据结构实验之链表三:链表的逆置

数据结构实验之链表三:链表的逆置 Time Limit: 1000MS Memory limit: 65536K 题目描述 输入多个整数,以-1作为结束标志,顺序建立一个带头结点的单链表,之后对该单链表的数据进行逆置,并输出逆置后的单链表数据. 输入 输入多个整数,以-1作为结束标志. 输出 输出逆置后的单链表数据. 示例输入 12 56 4 6 55 15 33 62 -1 示例输出 62 33 15 55 6 4 56 12 提示 不得使用数组. 来源 示例程序 /*************

数据结构实验之链表四:有序链表的归并

数据结构实验之链表四:有序链表的归并 Time Limit: 1000MS Memory limit: 65536K 题目描述 分别输入两个有序的整数序列(分别包含M和N个数据),建立两个有序的单链表,将这两个有序单链表合并成为一个大的有序单链表,并依次输出合并后的单链表数据. 输入 第一行输入M与N的值: 第二行依次输入M个有序的整数: 第三行依次输入N个有序的整数. 输出 输出合并后的单链表所包含的M+N个有序的整数. 示例输入 6 5 1 23 26 45 66 99 14 21 28 5

《数据结构教程》(李春葆 主编)课后习题【练习题6】

[6.5] 1 #include <iostream> 2 3 using namespace std; 4 #define MAXN 100 5 #define N 4 6 #define M 4 7 int x,y,num; 8 int a[MAXN][MAXN] = { 9 {0,2,3,4}, 10 {1,5,6,7}, 11 {8,9,10,11}, 12 {12,13,14,15}}; 13 bool FindX(int X) 14 { 15 while(a[x][y]!=X){

【数据结构】静态链表

数据结构之静态链表实现 前言 静态链表,是一种巧妙的数据结构实现方式. 静态链表: 每个节点有一个数据域(data),用来存放有用的数据信息: 还有一个下标域(cur),用来指示下一个元素的下标位置. 我们都知道链式线性表的优势在于插入和删除元素,时间复杂度都是O(1),因为不需要像顺序存储结构的线性表那样在插入和删除时需要移动大量的元素. 而静态链表的实现就弥补了这样的不足,我们以下标的方式来代替链式结构中使用的的指针,从而达到减少时间复杂度的功能. 分析 实现静态链表 下面是代码,实现了最基