《算法与数据结构 C语言描述》第二章 线性表

2.1 基本概念与抽象数据类型

2.1.1 基本概念

线性表简称为表,是零个或多个元素(表目)的有穷序列。通常表示为:
L=(k0, k1, ..., kn-1)
线性表的逻辑结构可以用二元组L=<K,R>来表示,其中k={k0, k1, ..., kn-1}, R={<ki, ki+1> , 0 <= i <= n-2}, i称为ki的索引或下标。K中所包含元素的个数称为表的长度,长度为0的表称为空表。

2.1.2 抽象数据类型

线性表的抽象数据类型如下:

ADT List is
Operations
    // 创建并返回一个空线性表
    List createNullList(void)
    // 插入元素
    int insertPre(List list, position p, DataType x)
    // 插入元素
    int insertPost(List list, position p, DataType x)
    //  删除元素
    int deleteV(List list, DataType x)
    // 删除元素
    int deleteP(List list, position p)
    // 查找位置
   position locate(List list, DataType x)
    // 判断是否为空
    int isNull(List list)
end ADT List

2.2 顺序表示

2.2.1 存储结构

在顺序表中,假设每个元素占用c个存储单元,则下标为i+1的元素存储位置与下标为i的元素存储位置之间,满足下列关系:
loc(ki+1) = loc(ki) + c
通常把顺序表k0的存储位置loc(k0),称为线性表的首地址或基地址,下标为i的元素ki的存储位置为:
loc(ki) = loc(k0) + i * c

定义如下:

struct SeqList {
    int MAXNUM;  // 顺序表中最大元素的个数
    int n;    // 存放线性表中元素的个数
    DataType *element; // 存放线性表中的元素
};

2.2.2 运算的实现

创建空的顺序表

SeqList* createNullList_seq(int n) {
    SeqList *pList = (SeqList *)malloc(sizeof(struct SeqList));
    if (pList != NULL) {
        pList->element = (DataType *) malloc(sizeof(DataType * n));
        if (pList->element) {
            pList->MAXNUM = n;
            pList->n = 0;
            return pList;
        } else {
            free pList;
        }
    }
    return NULL;
} 

判断顺序表是否为空

int isNullList_seq(SeqList *pList) {
    return (pList->n == 0);
}

求顺序表的位置

int locate_seq(SeqList *pList, DataType x) {
    int q;
    for(q = 0; q < pList->n; q++) {
        if (pList->element[q] == x) {
            return q;
        }
    }
    return -1;
}

顺序表的插入

int insertPre_seq(SeqList *pList, int p, DataType x) {
    int q;
    if (pList->n >= pList->MAXNUM) {
        printf("Overflow!\n");
        return 0;
    }
    if (isNullList_seq(pList)) {
        pList->element[0] = x;
        pList->n = 1;
        return 1; 
    }
    if (p < 0 || p > pList->n-1) {
        printf("Not exit!\n");
        return 0;
    }
    for(q = pList->n-1; q >= p; q--) {
        pList->element[q+1] = pList->element[q];
    }
    pList->element[p] = x;
    pList->n = pList->n+1;
    return 1;
}

顺序表的删除

int deleteP_seq(SeqList *pList, int p) {
    int q;
    if (p < 0 || p > pList->n-1) {
        printf("Not exit!\n");
        return 0;
    }
    for(q = p; q < pList->n-1; q++) {
        pList->element[q] = pList->element[q+1];
    }
    pList->n = pList->n - 1;
    return 1;
}

2.2.3 分析与评价

在有n个元素的线性表里,在下标为i(第i + 1个)的元素之前插入一个元素,需要移动n-i个元素,删除下标为i(第i+1个)的元素需要移动n-i-1个元素。
插入的平均移动数是:
Mi = ∑(n -i)Pi, i=0 ~ n
删除的平均移动数是:
Md = ∑(n - i - 1) Pi', i = 0 ~ n-1
可以求得
Mi = n/2,Md = (n - 2) / 2
故插入和删除的时间代价为O(n)

2.2.4 顺序表空间的扩展

申请一个比较大的数组,用新数组代替原来的数组,先把原来数组中所有元素复制到新数组的前面,然后继续插入
扩建的代码如下:

pos1 = (DataType *) malloc(sizeof(DataType) * pList->MAXNUM * 2);
if (pos1 == null) {
    return 0;
}
for(q = 0; q < pList->MAXNUM; q++) {
    pos1[q] = pList->element[q];
}
free(pList->element);
pList->element = pos1;
pList->MAXNUM *= 2;

2.3 链接表示

2.3.1 单链表表示

单链表的类型如下:

typedef struct Node {
    DataType info;
    struct Node* link;
} Node;
typedef Node* LinkList;

2.3.2 单链表上运算的实现

创建空链表

LinkList createNullList_link(void) {
    LinkList list = (LinkList) malloc(sizeof(struct Node));
    if (list != NULL) {
        list->link = NULL;
    }
    return list;
}

判断单链表是否为空

int isNullList_link(LinkList list) {
    return (list->link == NULL);
}

在单链表中求得某元素的存储位置

Node * locate_link(LinkList list, DataType x) {
    Node *p;
    if (list == NULL) {
        return NULL;
    }
    p = list->link;
    while (p != NULL && p->info != x) {
        p = p->link;
    }
    return p;
}

单链表的插入

int insertPost_link(LinkList list, Node *p, DataType x) {
    Node *q = (Node *) malloc(sizeof(Node));
    if (q == NULL) {
        printf("Out of space!\n");
        return 0;
    } else {
        q->info = x;
        q->link = p->link;
        p->link = q;
        return 1;
    }
}

在单链表中求p所指结点的前驱结点

Node *locatePre_link(LinkList list, Node *p) {
    Node *p1;
    if (list == NULL) {
        return NULL;
    }
    p1 = list;
    while (p1 != NULL && p1->link != p) {
        p1 = p1->link;
    }
    return p1;
}

单链表的删除

int deleteV_link(LinkList list, DataType x) {
    Node *p, *q;
    p = list;
    if (p == NULL) {
        return 0;
    }
    while (p->link != NULL && p->link->info != x) {
        p = p->link;
    }
    if (p->link == NULL) {
        return 0;
    } else {
        q = p->link;
        p->link = q->link;
        free(q);
        return 1;
    }
}

2.3.3 分析与比较

单链表的查找、插入、删除操作的时间复杂度为 O(n)

2.3.4 单链表的改进与扩充

循环链表

单链表的最后一个结点的指针指向头结点就得到循环链表。循环链表没有增加新的存储空间,但从循环链表中任一结点出发,都能访问所有结点。

双链表

双链表在单链表的基础上,增加一个指向前驱结点的指针。定义如下:

typedef struct DoubleNode {
    DataType info;
    struct DoubleNode *llink, rlink;
} Double Node;

双链表结构如下所示:


双链表

循环双链表

循环双链表是在双链表的基础上将头结点和尾结点连起来。


加入头结点的循环双链表

2.4 应用举例

2.4.1 Josphus问题

2.4.2 采用顺序表模拟

2.4.3 采用循环链表模拟

2.5 矩阵

2.5.1 矩阵的顺序表示

采用顺序存储方式表示矩阵,一般有行优先顺序和列优先顺序两种

行优先顺序即元素按行向量顺序排列,第i+1个行向量紧接在第i个行向量后面。矩阵的元素排列顺序如下:
a0 0, a0 1, ..., a0 n-1, a1 0, a1 1, ..., a1 n-1, ..., am-1 0, am-1 1, ..., am-1 n-1
列优先顺序即元素按向量顺序排列,第j+1个列向量紧接在第i个列向量后面。

按行优先顺序存储,地址计算公式如下:
loc(aij) = loc(a00) + i x n + j

按列优先顺序存储,地址计算公式如下:
loc(aij) = loc(a00) + j x m + i

2.5.2 稀疏矩阵的表示方法

设mxn的矩阵A中有k个非零元素,又设k大大小于mxn(记为k<<mxn),则称A为稀疏矩阵。存储稀疏矩阵的方法是多种多样的,下面介绍其中几种:

三元组表示法

用一个数组(顺序数据结构)来表示稀疏矩阵。这个数组中只存放稀疏矩阵的非零元素。每个结点包含3个字段,分别为该非零元素的行下标、列下标和值。结点间按行优先顺序排列

伪地址表示法

元素的伪地址就是在矩阵里按行优先顺序的相对位置(包括零元素一起算)。

带辅助行向量的二元组表示法

这个方法是三元组表示法的一个变种。在三元组表示法中,结点的行下标字段是按递增顺序排列的,列下标没有规律,因此可以考虑去掉行下标字段,而加上一个有m+1个元素的辅助行向量NRA,定义如下:
NRA[0] = 0;
NRA[i] = NRA[i - 1] + 矩阵第 i-1行中非零元素的个数(i > 0)

行-列表示法

稀疏矩阵行列表示法

2.6 广义表与动态存储管理

广义表是线性表的推广,具有广泛的应用价值。它是表处理语言的基础结构,也可以用于表示动态存储空间的整体模型。

2.6.1 广义表

广义表也是零个或多个元素组成的序列。但与线性表不同,广义表中的元素允许以不同形式出现:它可以是一个院子(逻辑上不能再分解的元素),也可以又是一个广义表。作为广义表元素的广义表称为广义表的子(广义)表。一个广义表还允许直接或间接地作为它自身的子(广义)表。
一个广义表中所包含的元素的个数,称为这个广义表的长度。长度为零的广义表称为空(广义)表。
为了区分广义表和原子,可以用圆括号把一个广义表括起来,再用逗号来分隔广义表中的元素。一个广义表的深度,就是指广义表中所含括号的层数。
例如:

E=()
L=(a, b)
A=(x, L) = (x, (a,b))
B=(A, y) = ((x, (a, b)), y)
C=(A, B) = ((x, (a, b)), ((x, (a, b)), y))
D=(z, D) =(z, (z, (z, ...)))

但要注意的是:

E1 = (E) = (())

E1不是空广义表,它有一个空广义表作为唯一的元素,所以长度为1
线性表:广义表中的元素全部都是原子
纯表:广义表中的元素允许有子广义表,但所有各层子广义表均无共享
再入表:在各层子广义表中允许共享的广义表
递归表:允许广义表直接地作为自己的子广义表

单链表示法

每个结点由三个字段组成:
| atom | info | link |
其中atom是标志位:atom = 0 表示本结点为子广义表,这时字段info存放本原子的信息(当信息量较大是,也可以存放本原子信息存放的地址)。字段link存放于本元素同层的下一个元素所对应结点的地址,当本元素是所在层的最后一个元素时,link = NULL。
如下图所示:


广义表的单链表示法

带头结点的单链表示法

单链表示法主要的缺点是:如果要删除广义表(或子广义表)中某一元素,则需要搜索广义表中所有结点后才能进行。


带头结点的单链表示法

2.6.2 结点的动态分配与回收

原则上说,单链表中的元素个数是任意的,允许自由变化的。习惯上把这种使用期间可自由插入和删除的数据结构称为动态数据结构。在程序运行过程中,对于动态数据结构结点的分配和回收需要采用动态存储管理的方法。

等长结点的分配与回收

当所有结点都是等长时,管理方法比较简单。
在系统运行初期,可以把整个可供链表动态分配的所有空间(称为动态区),按结点node类型的大小链接起来,构成一个单链表尘给可利用空间包(avail)。它与一般的链表差别在于:它是动态存储管理系统中,用于管理和记录可以分配的结点空间的单链表,不是应用程序中定义的单链表,不包括任何用户定义的信息,链接的顺序也无关紧要。当需要向某一个单链表插图一个结点时,可先从avail表中删除一个结点,然后把这个结点的空间提供给新插入结点使用;反过来,当从某个单链表中删除一个结点时,只要将这个结点从单链表中删除,然后送回到avail表中即可。

不等长结点的分配与回收

在实际应用中,往往要用到多个链表,而这些链表中的结点长度可能各不相同。这种情况下,一个简便的处理方法是,在动态区中建立多个可利用空间表,每个可利用空间表对应一种固定长度的结点。缺点是,难以真正解决多个可利用空间表的共享问题。系统开销也比较大。
另一种处理不等长结点的办法是:组织一个可以管理各种大小结点的可利用空间表。这种处理方法的优点是可以解决存储空间的共享问题,缺点是分配和回收的算法复杂了。另外,在系统长期动态运行过程中,这种方法有可能使整个空间被分割成许多大小不等的碎块,而某些碎块由于太小而长期得不到使用,则就产生了碎片问题。

动态分配

为了分配一个大小为N的结点,必须检索可利用空间表中的结点,找出一个size的值大于等于N的可利用块进行分配,同时把多余部分留在可利用空间表中;如果可利用空间表中不存在这样的结点则不能分配,称之为分配失败。常用的查找方法有三种:

  1. 最佳适配。检索可利用空间表中的全部结点,在所有size >= N的块中,找出最小的一块进行分配
  2. 首先适配。如果在可利用空间表中,找到第一个size >= N 的可利用块就进行分配
  3. 最大适配。如果在可利用空间表中,每次都用最大的可利用块来分配,当最大的块长度小于N时,则分配失败。

2.6.3 废料收集与存储压缩

废料收集

从上面的讨论可以看出,系统的动态去主要由两部分构成:一部分是已利用空间构成的广义表,它可能被许多程序共享,每个程序可能同时使用多个不同方式定义的链表;另一部分是可利用空间构成的广义表,它可能包含多个大小不同的可利用空间表,也可能包含多个不等长结点的可利用空间表。
在动态区中,还可能在一些被无用结点占用的空间,简称为无用结点(也称为废料)。
废料收集需要解决的问题是:从整个动态区的已利用空间中,找出哪些结点是无用结点,并且把它们送入可利用空间表中。

存储压缩

碎片与废料不同,它出现在可利用空间中,但因为太小了,长期无法分配使用。虽然在动态分配和回收过程中,系统采用各种方法来减少碎片的产生,但都难以杜绝碎片的出现。为了从根本上解决碎片问题,可以采用存储压缩的方法。
压缩存储就是把有用的结点压缩到动态区的一端,把可利用的结点(包括碎片)压缩到动态区的另一端,使全部可利用的空间连成一片,构成一个大的可利用块。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容