02-数据结构与算法(线性表学习资料整理)

一.线性表

线性表是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。线性表的逻辑结构简单,便于实现和操作。因此,线性表这种数据结构在实际应用中是广泛采用的一种数据结构。

1.1结构

线性表是一种常用的数据结构,以下介绍线性表及其顺序存储,并对栈和队列及它们的顺序实现给出了详细的设计描述。

在实际应用中,线性表都是以队列字符串等特殊线性表的形式来使用的。由于这些特殊线性表都具有各自的特性,因此,掌握这些特殊线性表的特性,对于数据运算的可靠性和提高操作效率都是至关重要的。

线性表是一个线性结构,它是一个含有n≥0个结点的有限序列,对于其中的结点,有且仅有一个开始结点没有前驱但有一个后继结点,有且仅有一个终端结点没有后继但有一个前驱结点,其它的结点都有且仅有一个前驱和一个后继结点。一般地,一个线性表可以表示成一个线性序列:k1,k2,…,kn,其中k1是开始结点,kn是终端结点。

是一个数据元素的有序(次序)集

1.2特征

线性结构的基本特征为:

1.集合中必存在唯一的一个"第一元素";

2.集合中必存在唯一的一个 "最后元素" ;

3.除最后一个元素之外,均有 唯一的后继(后件);

4.除第一个元素之外,均有 唯一的前驱(前件)。

由n(n≥0)个数据元素(结点)a1,a2,…,an组成的有限序列。

数据元素的个数n定义为表的长度。

当n=0时称为空表。

常常将非空的线性表(n>0)记作:

(a1,a2,…an)

数据元素ai(1in)只是一个抽象的符号,其具体含义在不同的情况下可以不同。

线性表的基本操作

1)MakeEmpty(L) 这是一个将L变为空表的方法

2)Length(L) 返回表L的长度,即表中元素个数

3)Get(L,i) 这是一个函数,函数值为L中位置i处的元素(1≤i≤n)

4)Prev(L,i) 取i的前驱元素

5)Next(L,i) 取i的后继元素

6)Locate(L,x) 这是一个函数,函数值为元素x在L中的位置

7)Insert(L,i,x)在表L的位置i处插入元素x,将原占据位置i的元素及后面的元素都向后推一个位置

8)Delete(L,p) 从表L中删除位置p处的元素

9)IsEmpty(L) 如果表L为空表(长度为0)则返回true,否则返回false

10)Clear(L)清除所有元素

11)Init(L)同第一个,初始化线性表为空

12)Traverse(L)遍历输出所有元素

13)Find(L,x)查找并返回元素

14)Update(L,x)修改元素

15)Sort(L)对所有元素重新按给定的条件排序

16) strstr(string1,string2)用于字符数组的求string1中出现string2的首地址

1.3结构特点

 

线性表具有如下的结构特点:

1.均匀性:虽然不同数据表的数据元素可以是各种各样的,但对于同一线性表的各数据元素必定具有相同的数据类型和长度。

2.有序性:各数据元素在线性表中的位置只取决于它们的序号,数据元素之前的相对位置是线性的,即存在唯一的"第一个"和"最后一个"的数据元素,除了第一个和最后一个外,其它元素前面均只有一个数据元素直接前驱和后面均只有一个数据元素(直接后继)。

在实现线性表数据元素的存储方面,一般可用顺序存储结构和链式存储结构两种方法。链式存储结构将在本网站线性链表中介绍,本章主要介绍用数组实现线性表数据元素的顺序存储及其应用。另外栈、队列和串也是线性表的特殊情况,又称为受限的线性结构。

附一道选择题:

下列哪个不是线性表(D)

A. 链表 B. 队列 C.栈 D.关联数组

1.4线性表的推广

 

时间有序表、排序表、和频率有序表都可以看做是线性表的推广。如果按照结点到达结构的时间先后,作为确定结点之间关系的,这样一种线性结构称之为时间有序表。例如,在红灯前停下的一长串汽车,最先到达的为首结点,最后到达的为尾结点;在离开时最先到达的汽车将最先离开,最后到达的将最后离开。这些汽车构成理一个队列,实际上就是一个时间有序表。栈和队列都是时间有序表。频率有序表是按照结点的使用频率确定它们之间的相互关系的,而排序表是根据结点的关键字值来加以确定的。

 

二.顺序存储结构

2.1介绍

在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构.

顺序存储结构是存储结构类型中的一种,该结构是把逻辑上相邻的节点存储在物理位置上相邻的存储单元中,结点之间的逻辑关系由存储单元的邻接关系来体现。由此得到的存储结构为顺序存储结构,通常顺序存储结构是借助于计算机程序设计语言(例如c/c++)的数组来描述的。

顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。

优点:随机存取表中元素。缺点:插入和删除操作需要移动元素。

 

2.2C#语言环境顺序存储结构对象举例:

  1. Array
    1. 引用类型(托管堆)
    2. 初始化时会设置默认
  2. 自定义Array的代码实现

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

namespace ConsoleApplication2

{

class MyArrayList

{

//容量

private const int _defaultCapacity = 4;

//存放数组元素

private object[] _items;

//数组大小

private int _size;

//元素个数为0的数组状态

private static readonly object[] emptyArray = new object[0];

 

public MyArrayList()

{

this._items = emptyArray;

}

 

public MyArrayList( int capacity)

{

if (capacity<0)

{

throw new ArgumentOutOfRangeException("capacity","ArrayList的容量不可为负数!");

}

this._items = new object[capacity];

}

 

//索引器

public virtual object this[int index]

{

get

{

if (index<0||index>=this._size)

{

throw new ArgumentOutOfRangeException("index","索引超出范围!");

}

return this._items[index];

}

 

set

{

if (index < 0 || index >= this._size)

{

throw new ArgumentOutOfRangeException("index", "索引超出范围!");

}

this._items[index] = value;

}

 

}

 

//当前数组元素个数

public virtual int Count

{

get {return this._size ;}

}

 

//数组的容量

public virtual int Capacity

{

get { return this._items.Length; }

set

{

if (value!=this._items.Length)

{

if (value<this._size)

{

throw new ArgumentOutOfRangeException("value","容量太小");

}

if (value > 0)

{//开辟新内存空间存储元素

object[] dest = new object[value];

if (this._size > 0)

{//搬动元素

Array.Copy(this._items, 0, dest, 0, this._size);

}

this._items = dest;

}

else//数组最小的空间为4

{

this._items=new object[_defaultCapacity];

}

}

}

}

 

//元素的添加

public virtual int Add(object value)

{//当空间已满

if (this._size==this._items.Length)

{

this.EnsureCapacity(this._size+1);

}

this._items[this._size] = value;

return this._size++;

}

 

//扩容

private void EnsureCapacity(int p)

{

if (this._items.Length<p)

{//空间加倍

int num = (this._items.Length == 0) ? _defaultCapacity : (this._items.Length * 2);

if (num < p)

{

num = p;

}

this.Capacity = num;

}

}

 

//指定位置插入元素

public virtual void Insert( int index,object value)

{

if (index<0||index>this._size)

{

throw new ArgumentOutOfRangeException("index","索引超出范围!");

}

if (this._size==this._items.Length)

{

this.EnsureCapacity(this._size + 1);

}

if (index<this._size)

{

Array.Copy(this._items, index, this._items, index + 1, this._size - index);

}

this._items[index] = value;

this._size++;

}

 

//移除指定索引的元素

public virtual void Remove(int index)

{

if (index < 0 || index > this._size)

{

throw new ArgumentOutOfRangeException("index", "索引超出范围!");

}

this._size--;

if (index<this._size)

{

Array.Copy(this._items,index+1,this._items,index,this._size-index);

}

this._items[this._size]=null;

}

 

//裁剪空间

public virtual void TrimToSize()

{

this.Capacity = this._size;

}

}

}

三.链式存储结构

3.1介绍

在计算机中用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的).

它不要求逻辑上相邻的元素在物理位置上也相邻.因此它没有顺序存储结构所具有的弱点,但也同时失去了顺序表随机存取的优点.

3.2链式存储结构特点

1、比顺序存储结构的存储密度小 (每个节点都由数据域和指针域组成,所以相同空间内假设全存满的话顺序比链式存储更多)。

2、逻辑上相邻的节点物理上不必相邻。

3、插入、删除灵活 (不必移动节点,只要改变节点中的指针)。

4、查找结点时链式存储要比顺序存储慢。

5、每个结点是由数据域和指针域组成。

3.3链式存储结构分类介绍

3.3.1单向链表

3.3.1.1.概念

单向链表(单链表)是链表的一种,其特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始;链表是使用指针进行构造的列表;又称为结点列表,因为链表是由一个个结点组装起来的;其中每个结点都有指针成员变量指列表中的下一个结点;

列表是由结点构成,由head指针指向第一个成为表头的结点而终止于最后一个指向nuLL的指针;

3.3.1.2.单向链表C语言实例:

/*

===============================================

目的:学习单向链表的创建、删除、

插入(无序、有序)、输出、

排序(选择、插入、冒泡)、反序

排序(选择、插入、冒泡)

插入(有序)

================================================

*/

/*

单向链表的图示:

---->[NULL]

head

图1:空链表

---->[p1]---->[p2]...---->[pn]---->[NULL]

head p1->next p2->next pn->next

图2:有N个结点的链表

*/

#include <stdlib.h>

#include <stdio.h>

#define NULL 0

#define LEN sizeof(struct student)

struct student

{

long num; /*学号 */

float score; /*分数,其他信息可以继续在下面增加字段 */

struct student *next; /*指向下一结点的指针 */

};

int n; /*结点总数 */

/*

==========================

功能:创建结点

返回:指向链表表头的指针

==========================

*/

struct student *

Create ()

{

struct student *head; /*头结点 */

struct student *p1 = NULL; /*p1保存创建的新结点的地址 */

struct student *p2 = NULL; /*p2保存原链表最后一个结点的地址 */

n = 0; /*创建前链表的结点总数为0:空链表 */

p1 = (struct student *) malloc (LEN); /*开辟一个新结点 */

p2 = p1; /*如果结点开辟成功,则p2先把它的指针保存下来以备后用 */

if (p1 == NULL) /*结点开辟不成功 */

{

printf ("\nCann't create it, try it again in a moment!\n");

return NULL;

}

else /*结点开辟成功 */

{

head = NULL; /*开始head指向NULL */

printf ("Please input %d node -- num,score: ", n + 1);

scanf ("%ld,%f", &(p1->num), &(p1->score)); /*录入数据 */

}

while (p1->num != 0) /*只要学号不为0,就继续录入下一个结点 */

{

n += 1; /*结点总数增加1个 */

if (n == 1) /*如果结点总数是1,则head指向刚创建的结点p1 */

{

head = p1;

/*

注意:

此时的p2就是p1,也就是p1->next指向NULL。

这样写目的是与下面else保持一致。

*/

p2->next = NULL;

}

else

{

p2->next = p1; /*指向上次下面刚开辟的结点 */

}

p2 = p1; /*把p1的地址给p2保留,然后p1去产生新结点 */

p1 = (struct student *) malloc (LEN);

printf ("Please input %d node -- num,score: ", n + 1);

scanf ("%ld,%f", &(p1->num), &(p1->score));

}

p2->next = NULL; /*此句就是根据单向链表的最后一个结点要指向NULL */

free (p1); /*释放p1。用malloc()、calloc()的变量都要free() */

p1 = NULL; /*特别不要忘记把释放的变量清空置为NULL,否则就变成"野指针",即地址不确定的指针。 */

return head; /*返回创建链表的头指针 */

}

/*

===========================

功能:输出结点

返回: void

===========================

*/

void

Print (struct student *head)

{

struct student *p;

printf ("\nNow , These %d records are:\n", n);

p = head;

if (head != NULL) /*只要不是空链表,就输出链表中所有结点 */

{

printf ("head is %o\n", head); /*输出头指针指向的地址 */

do

{

/*

输出相应的值:当前结点地址、各字段值、当前结点的下一结点地址。

这样输出便于读者形象看到一个单向链表在计算机中的存储结构,和我们

设计的图示是一模一样的。

*/

printf ("%o %ld %5.1f %o\n", p, p->num, p->score, p->next);

p = p->next; /*移到下一个结点 */

}

while (p != NULL);

}

}

/*

==========================

功能:删除指定结点

(此例中是删除指定学号的结点)

返回:指向链表表头的指针

==========================

*/

/*

单向链表的删除图示:

---->[NULL]

head

图3:空链表

从图3可知,空链表显然不能删除

---->[1]---->[2]...---->[n]---->[NULL](原链表)

head 1->next 2->next n->next

---->[2]...---->[n]---->[NULL](删除后链表)

head 2->next n->next

图4:有N个结点的链表,删除第一个结点

结合原链表和删除后的链表,就很容易写出相应的代码。操作方法如下:

1、你要明白head就是第1个结点,head->next就是第2个结点;

2、删除后head指向第2个结点,就是让head=head->next,OK这样就行了。

---->[1]---->[2]---->[3]...---->[n]---->[NULL](原链表)

head 1->next 2->next 3->next n->next

---->[1]---->[3]...---->[n]---->[NULL](删除后链表)

head 1->next 3->next n->next

图5:有N个结点的链表,删除中间一个(这里图示删除第2个)

结合原链表和删除后的链表,就很容易写出相应的代码。操作方法如下:

1、你要明白head就是第1个结点,1->next就是第2个结点,2->next就是第3个结点;

2、删除后2,1指向第3个结点,就是让1->next=2->next。

*/

struct student *

Del (struct student *head, long num)

{

struct student *p1; /*p1保存当前需要检查的结点的地址 */

struct student *p2; /*p2保存当前检查过的结点的地址 */

if (head == NULL) /*是空链表(结合图3理解) */

{

printf ("\nList is null!\n");

return head;

}

/*定位要删除的结点 */

p1 = head;

while (p1->num != num && p1->next != NULL) /*p1指向的结点不是所要查找的,并且它不是最后一个结点,就继续往下找 */

{

p2 = p1; /*保存当前结点的地址 */

p1 = p1->next; /*后移一个结点 */

}

if (num == p1->num) /*找到了。(结合图4、5理解) */

{

if (p1 == head) /*如果要删除的结点是第一个结点 */

{

head = p1->next; /*头指针指向第一个结点的后一个结点,也就是第二个结点。这样第一个结点就不在链表中,即删除。 */

}

else /*如果是其它结点,则让原来指向当前结点的指针,指向它的下一个结点,完成删除 */

{

p2->next = p1->next;

}

free (p1); /*释放当前结点 */

p1 = NULL;

printf ("\ndelete %ld success!\n", num);

n -= 1; /*结点总数减1个 */

}

else /*没有找到 */

{

printf ("\n%ld not been found!\n", num);

}

return head;

}

/*

==========================

功能:插入指定结点的后面

(此例中是指定学号的结点)

返回:指向链表表头的指针

==========================

*/

/*

单向链表的插入图示:

---->[NULL](原链表)

head

---->[1]---->[NULL](插入后的链表)

head 1->next

图7 空链表插入一个结点

结合原链表和插入后的链表,就很容易写出相应的代码。操作方法如下:

1、你要明白空链表head指向NULL就是head=NULL;

2、插入后head指向第1个结点,就是让head=1,1->next=NULL,OK这样就行了。

---->[1]---->[2]---->[3]...---->[n]---->[NULL](原链表)

head 1->next 2->next 3->next n->next

---->[1]---->[2]---->[x]---->[3]...---->[n]---->[NULL](插入后的链表)

head 1->next 2->next x->next 3->next n->next

图8:有N个结点的链表,插入一个结点(这里图示插入第2个后面)

结合原链表和插入后的链表,就很容易写出相应的代码。操作方法如下:

1、你要明白原1->next就是结点2,2->next就是结点3;

2、插入后x指向第3个结点,2指向x,就是让x->next=2->next,1->next=x。

*/

struct student *

Insert (struct student *head, long num, struct student *node)

{

struct student *p1; /*p1保存当前需要检查的结点的地址 */

if (head == NULL) /*(结合图示7理解) */

{

head = node;

node->next = NULL;

n += 1;

return head;

}

p1 = head;

while (p1->num != num && p1->next != NULL) /*p1指向的结点不是所要查找的,并且它不是最后一个结点,继续往下找 */

{

p1 = p1->next; /*后移一个结点 */

}

if (num == p1->num) /*找到了(结合图示8理解) */

{

node->next = p1->next; /*显然node的下一结点是原p1的next */

p1->next = node; /*插入后,原p1的下一结点就是要插入的node */

n += 1; /*结点总数增加1个 */

}

else

{

printf ("\n%ld not been found!\n", num);

}

return head;

}

/*

==========================

功能:反序结点

(链表的头变成链表的尾,链表的尾变成头)

返回:指向链表表头的指针

==========================

*/

/*

单向链表的反序图示:

---->[1]---->[2]---->[3]...---->[n]---->[NULL](原链表)

head 1->next 2->next 3->next n->next

[NULL]<----[1]<----[2]<----[3]<----...[n]<----(反序后的链表)

1->next 2->next 3->next n->next head

图9:有N个结点的链表反序

结合原链表和插入后的链表,就很容易写出相应的代码。操作方法如下:

1、我们需要一个读原链表的指针p2,存反序链表的p1=NULL(刚好最后一个结点的next为NULL),还有一个临时存储变量p;

2、p2在原链表中读出一个结点,我们就把它放到p1中,p就是用来处理结点放置顺序的问题;

3、比如,现在我们取得一个2,为了我们继续往下取结点,我们必须保存它的next值,由原链表可知p=2->next;

4、然后由反序后的链表可知,反序后2->next要指向1,则2->next=1;

5、好了,现在已经反序一个结点,接着处理下一个结点就需要保存此时的信息:

p1变成刚刚加入的2,即p1=2;p2要变成它的下一结点,就是上面我们保存的p,即p2=p。

*/

struct student *

Reverse (struct student *head)

{

struct student *p; /*临时存储 */

struct student *p1; /*存储返回结果 */

struct student *p2; /*源结果结点一个一个取 */

p1 = NULL; /*开始颠倒时,已颠倒的部分为空 */

p2 = head; /*p2指向链表的头结点 */

while (p2 != NULL)

{

p = p2->next;

p2->next = p1;

p1 = p2;

p2 = p;

}

head = p1;

return head;

}

/*

==========================

功能:选择排序(由小到大)

返回:指向链表表头的指针

==========================

*/

/*

选择排序的基本思想就是反复从还未排好序的那些结点中,

选出键值(就是用它排序的字段,我们取学号num为键值)最小的结点,

依次重新组合成一个链表。

我认为写链表这类程序,关键是理解:

head存储的是第一个结点的地址,head->next存储的是第二个结点的地址;

任意一个结点p的地址,只能通过它前一个结点的next来求得。

单向链表的选择排序图示:

---->[1]---->[3]---->[2]...---->[n]---->[NULL](原链表)

head 1->next 3->next 2->next n->next

---->[NULL](空链表)

first

tail

---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序后链表)

first 1->next 2->next 3->next tail->next

图10:有N个结点的链表选择排序

1、先在原链表中找最小的,找到一个后就把它放到另一个空的链表中;

2、空链表中安放第一个进来的结点,产生一个有序链表,并且让它在原链表中分离出来(此时要注意原链表中出来的是第一个结点还是中间其它结点);

3、继续在原链表中找下一个最小的,找到后把它放入有序链表的尾指针的next,然后它变成其尾指针;

*/

struct student *

SelectSort (struct student *head)

{

struct student *first; /*排列后有序链的表头指针 */

struct student *tail; /*排列后有序链的表尾指针 */

struct student *p_min; /*保留键值更小的结点的前驱结点的指针 */

struct student *min; /*存储最小结点 */

struct student *p; /*当前比较的结点 */

first = NULL;

while (head != NULL) /*在链表中找键值最小的结点。 */

{

/*注意:这里for语句就是体现选择排序思想的地方 */

for (p = head, min = head; p->next != NULL; p = p->next) /*循环遍历链表中的结点,找出此时最小的结点。 */

{

if (p->next->num < min->num) /*找到一个比当前min小的结点。 */

{

p_min = p; /*保存找到结点的前驱结点:显然p->next的前驱结点是p。 */

min = p->next; /*保存键值更小的结点。 */

}

}

/*上面for语句结束后,就要做两件事;一是把它放入有序链表中;二是根据相应的条件判断,安排它离开原来的链表。 */

/*第一件事 */

if (first == NULL) /*如果有序链表目前还是一个空链表 */

{

first = min; /*第一次找到键值最小的结点。 */

tail = min; /*注意:尾指针让它指向最后的一个结点。 */

}

else /*有序链表中已经有结点 */

{

tail->next = min; /*把刚找到的最小结点放到最后,即让尾指针的next指向它。 */

tail = min; /*尾指针也要指向它。 */

}

/*第二件事 */

if (min == head) /*如果找到的最小结点就是第一个结点 */

{

head = head->next; /*显然让head指向原head->next,即第二个结点,就OK */

}

else /*如果不是第一个结点 */

{

p_min->next = min->next; /*前次最小结点的next指向当前min的next,这样就让min离开了原链表。 */

}

}

if (first != NULL) /*循环结束得到有序链表first */

{

tail->next = NULL; /*单向链表的最后一个结点的next应该指向NULL */

}

head = first;

return head;

}

/*

==========================

功能:直接插入排序(由小到大)

返回:指向链表表头的指针

==========================

*/

/*

直接插入排序的基本思想就是假设链表的前面n-1个结点是已经按键值

(就是用它排序的字段,我们取学号num为键值)排好序的,对于结点n在

这个序列中找插入位置,使得n插入后新序列仍然有序。按照这种思想,依次

对链表从头到尾执行一遍,就可以使无序链表变为有序链表。

单向链表的直接插入排序图示:

---->[1]---->[3]---->[2]...---->[n]---->[NULL](原链表)

head 1->next 3->next 2->next n->next

---->[1]---->[NULL](从原链表中取第1个结点作为只有一个结点的有序链表)

head

图11

---->[3]---->[2]...---->[n]---->[NULL](原链表剩下用于直接插入排序的结点)

first 3->next 2->next n->next

图12

---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序后链表)

head 1->next 2->next 3->next n->next

图13:有N个结点的链表直接插入排序

1、先在原链表中以第一个结点为一个有序链表,其余结点为待定结点。

2、从图12链表中取结点,到图11链表中定位插入。

3、上面图示虽说画了两条链表,其实只有一条链表。在排序中,实质只增加了一个用于指向剩下需要排序结点的头指针first罢了。

这一点请读者务必搞清楚,要不然就可能认为它和上面的选择排序法一样了。

*/

struct student *

InsertSort (struct student *head)

{

struct student *firs

 

3.3.2.循环链表

循环链表是另一种形式的链式存贮结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。

3.3.2.1分类

  循环链表

1)单循环链表——在单链表中,将终端结点的指针域NULL改为指向表头结点或开始结点即可。

(2)多重链的循环链表——将表中结点链在多个环上。

3.3.2.22带头结点

判断空链表的条件是head==head->next;

3.3.2.3尾指针

用尾指针rear表示的单循环链表对开始结点a1和终端结点an查找时间都是O(1)。而表的操作常常是在表的首尾位置上进行,因此,实用中多采用尾指针表示单循环链表。带尾指针的单循环链表可见下图。

注意:判断空链表的条件为rear==rear->next;

3.3.2.4特点

循环链表的特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。

【例】在链表上实现将两个线性表(a1,a2,…,an)和(b1,b2,…,bm)连接成一个线性表(a1,…,an,b1,…bm)的运算。

分析:若在单链表或头指针表示的单循环表上做这种链接操作,都需要遍历第一个链表,找到结点an,然后将结点b1链到an的后面,其执行时间是O(n)。若在尾指针表示的单循环链表上实现,则只需修改指针,无须遍历,其执行时间是O(1)。

相应的算法如下:

LinkListConnect(LinkListA,LinkListB)

{//假设A,B为非空循环链表的尾指针

LinkListp=A->next;//①保存A表的头结点位置

A->next=B->next->next;//②B表的开始结点链接到A表尾

free(B->next);//③释放B表的头结点

B->next=p;//④

returnB;//返回新循环链表的尾指针

}

注意:

①循环链表中没有NULL指针。涉及遍历操作时,其终止条件就不再是像非循环链表那样判别p或p->next是否为空,而是判别它们是否等于某一指定指针,如头指针或尾指针等。

②在单链表中,从一已知结点出发,只能访问到该结点及其后续结点,无法找到该结点之前的其它结点。而在单循环链表中,从任一结点出发都可访问到表中所有结点,这一优点使某些运算在单循环链表上易于实现。

 

3.3.3.双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表

 

3.3.3.1链表的操作

1.线性表的双向链表存储结构

 

typedef struct DuLNode

{

ElemType data;

struct DuLNode *prior,*next;

}DuLNode,*DuLinkList;

带头结点的双向循环链表的基本操作

void InitList(DuLinkList *L)

{ /* 产生空的双向循环链表L */

L=(DuLinkList)malloc(sizeof(DuLNode));

if(*L)

(*L)->next=(*L)->prior=*L;

else

exit(OVERFLOW);

}

2.销毁双向循环链表L

 

void DestroyList(DuLinkList *L)

{

DuLinkList q,p=(*L)->next; /* p指向第一个结点 */

while(p!=*L) /* p没到表头 */

{

q=p->next;

free(p);

p=q;

}

free(*L);

*L=NULL;

}

3.重置链表为空表

 

void ClearList(DuLinkList L) /* 不改变L */

{ DuLinkList q,p=L->next; /* p指向第一个结点 */

while(p!=L) /* p没到表头 */

{

q=p->next;

free(p);

p=q;

}

L->next=L->prior=L; /*头结点的两个指针域均指向自身 */

}

4.验证是否为空表

 

Status ListEmpty(DuLinkList L)

{ /* 初始条件:线性表L已存在

if(L->next==L&&L->prior==L)

return TRUE;

else

return FALSE;

}

3.3.3.2元素的操作

1.计算表内元素个数

 

int ListLength(DuLinkList L)

{ /* 初始条件:L已存在。操作结果: */

int i=0;

DuLinkList p=L->next; /* p指向第一个结点 */

while(p!=L) /* p没到表头 */

{

i++;

p=p->next;

}

return i;

}

2.赋值

 

Status GetElem(DuLinkList L,int i,ElemType *e)

{ /* 当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR */

int j=1; /* j为计数器 */

DuLinkList p=L->next; /* p指向第一个结点 */

while(p!=L&&j<i)

{

p=p->next;

j++;

}

if(p==L||j>i) /* 第i个元素不存在 */

return ERROR;

*e=p->data; /* 取第i个元素 */

return OK;

}

3.查找元素

 

int LocateElem(DuLinkList L,ElemType e,Status(*compare)(ElemType,ElemType))

{ /* 初始条件:L已存在,compare()是数据元素判定函数 */

/* 操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。 */

/* 若这样的数据元素不存在,则返回值为0 */

int i=0;

DuLinkList p=L->next; /* p指向第1个元素 */

while(p!=L)

{

i++;

if(compare(p->data,e)) /* 找到这样的数据元素*/

return i;

p=p->next;

}

return 0;

}

4.查找元素前驱

 

Status PriorElem(DuLinkList L,ElemType cur_e,ElemType *pre_e)

{ /* 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱, */

/* 否则操作失败,pre_e无定义 */

DuLinkList p=L->next->next; /* p指向第2个元素 */

while(p!=L) /* p没到表头 */

{

if(p->data==cur_e)

{

*pre_e=p->prior->data;

return TRUE;

}

p=p->next;

}

return FALSE;

}

5.查找元素后继

 

Status NextElem(DuLinkList L,ElemType cur_e,ElemType *next_e)

{ /* 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继, */

/* 否则操作失败,next_e无定义 */

DuLinkList p=L->next->next; /* p指向第2个元素 */

while(p!=L) /* p没到表头 */

{

if(p->prior->data==cur_e)

{

*next_e=p->data;

return TRUE;

}

p=p->next;

}

return FALSE;

}

6.查找元素地址

 

DuLinkList GetElemP(DuLinkList L,int i) /* 另加 */

{ /* 在双向链表L中返回第i个元素的地址。i为0,返回头结点的地址。若第i个元素不存在,*/

/* 返回NULL */

int j;

DuLinkList p=L; /* p指向头结点 */

if(i<0||i>ListLength(L)) /* i值不合法 */

return NULL;

for(j=1;j<=i;j++)

p=p->next;

return p;

}

7.元素的插入

 

Status ListInsert(DuLinkList L,int i,ElemType e)

{ /* 在带头结点的双链循环线性表L中第i个位置之前插入元素e,i的合法值为1≤i≤表长+1 */

/* 改进算法2.18,否则无法在第表长+1个结点之前插入元素 */

DuLinkList p,s;

if(i<1||i>ListLength(L)+1) /* i值不合法 */

return ERROR;

p=GetElemP(L,i-1); /* 在L中确定第i个元素前驱的位置指针p */

if(!p) /* p=NULL,即第i个元素的前驱不存在(设头结点为第1个元素的前驱) */

return ERROR;

s=(DuLinkList)malloc(sizeof(DuLNode));

if(!s)

return OVERFLOW;

s->data=e;

s->prior=p; /* 在第i-1个元素之后插入 */

s->next=p->next;

p->next->prior=s;

p->next=s;

return OK;

}

8.元素的删除

 

Status ListDelete(DuLinkList L,int i,ElemType *e)

{ /* 删除带头结点的双链循环线性表L的第i个元素,i的合法值为1≤i≤表长 */

DuLinkList p;

if(i<1) /* i值不合法 */

return ERROR;

p=GetElemP(L,i); /* 在L中确定第i个元素的位置指针p */

if(!p) /* p=NULL,即第i个元素不存在 */

return ERROR;

*e=p->data;

p->prior->next=p->next;

p->next->prior=p->prior;

free(p);

return OK;

}

9.正序查找

 

void ListTraverse(DuLinkList L,void(*visit)(ElemType))

{ /* 由双链循环线性表L的头结点出发,正序对每个数据元素调用函数visit() */

DuLinkList p=L->next; /* p指向头结点 */

while(p!=L)

{

visit(p->data);

p=p->next;

}

printf("\n");

}

void ListTraverseBack(DuLinkList L,void(*visit)(ElemType))

10.逆序查找

 

{ /* 由双链循环线性表L的头结点出发,逆序对每个数据元素调用函数visit()。另加 */

DuLinkList p=L->prior; /* p指向尾结点 */

while(p!=L)

{

visit(p->data);

p=p->prior;

}

printf("\n");

}

3.3.3.3双向链表模板

/*****************************************************

*文件名:LinkedList.h

*功能:实现双向链表的基本功能

*注意:为了使最终程序执行得更快,仅在Debug模式下检测操作合法性。

*另外不对内存分配失败作处理,因为一般情况下应用程序有近2GB真正可用的空间

*********************************************************/

#pragma once

#include <assert.h>

template<class T>

class LinkedList

{

private:

class Node

{

public:

T data; //数据域,不要求泛型T的实例类有无参构造函数

Node* prior; //指向前一个结点

Node* next; //指向下一个结点

Node(const T& element,Node*& pri,Node*& nt):data(element),next(nt),prior(pri){}

Node():data(data){}//泛型T的实例类的复制构造函数将被调用.在Vc2010测试可行

};

Node* head; //指向第一个结点

public:

//初始化:构造一个空结点,搭建空链

LinkedList():head(new Node()){head->prior=head->next=head;}

//获取元素总数

int elementToatal()const;

//判断是否为空链

bool isEmpty()const{return head==head->next?true:false;}

//将元素添加至最后,注意node的指针设置

void addToLast(const T& element){Node* ne=new Node(element,head->prior,head);head->prior=head->prior->next=ne;}

//获取最后一个元素

T getLastElement()const{assert(!isEmpty());return head->prior->data;}

//删除最后一个元素,注意node的指针设置

void delLastElement(){assert(!isEmpty());Node* p=head->prior->prior;delete head->prior;head->prior=p;p->next=head;}

//修改最后一个元素

void alterLastEmlent(const T& newElement){assert(!isEmpty());head->prior->data=newElement;}

//插入元素

void insertElement(const T& element,int position);

//获取元素

T getElement(int index)const;

//删除元素

T delElement(int index);

//修改元素

void alterElement(const T & Newelement,int index);

//查找元素

int findElement(const T& element) const;

//正序遍历

void Traverse(void (*visit)(T&element));

//逆序遍历

void TraverseBack(void (*visit)(T&element));

//重载[]函数

T& operator [](int index);

//清空链表

void clearAllElement();

//销毁链表

~LinkedList();

};

/***************************************

*返回元素总数

****************************************/

template<class T>

int LinkedList<T>::elementToatal()const

{

int Total=0;

for(Node* p=head->next;p!=head;p=p->next) ++Total;

return Total;

}

/**********************************************

*在position指定的位置插入元素。原来position及后面的元

*素后移

***********************************************/

template<class T>

void LinkedList<T>::insertElement(const T& element,int position)

{

assert(position>0 && position<=elementToatal()+1);

Node* p=head;

while(position)

{

p=p->next;

position--;

}

//此时p指向要插入的结点

Node* pNew=new Node(element,p->prior,p);

p->prior=p->prior->next=pNew;

}

/***************************************

*返回找到的元素的副本

***************************************/

template<class T>

T LinkedList<T>::getElement(int index)const

{

assert(index>0 && index<=elementToatal() && !isEmpty());//位置索引是否合法,链表是否空

Node* p=head->next;

while(--index) p=p->next;

return p->data;

}

/**********************************

*删除指定元素,并返回它

**********************************/

template<class T>

T LinkedList<T>::delElement(int index)

{

assert(index>0 && index<=elementToatal() && !isEmpty());//位置索引是否合法,链表是否空

Node* del=head->next;

while(--index) del=del->next;

//此时p指向要删除元素

del->prior->next=del->next;

del->next->prior=del->prior;

T delData=del->data;

delete del;

return delData;

}

/****************************************

*用Newelement代替索引为index的元素

*****************************************/

template<class T>

void LinkedList<T>::alterElement(const T & Newelement,int index)

{

assert(index>0 && index<=elementToatal() && !isEmpty());//位置索引是否合法,链表是否空

Node* p=head->next;

while(--index) p=p->next;

p->data=Newelement;

}

/********************************

*找到返回元素的索引,否则返回0

********************************/

template<class T>

int LinkedList<T>::findElement(const T& element) const

{

Node* p=head->next;

int i=0;

while(p!=head)

{

i++;

if(p->data==element) return i;

p=p->next;

}

return 0;

}

/***************************************

*正向遍历,以链表中每个元素作为参数调用visit函数

*****************************************/

template<class T>

void LinkedList<T>::Traverse(void (*visit)(T&element))

{

Node* p=head->next;

while(p!=head)

{

visit(p->data);//注意此时外部visit函数有权限修改LinkedList<T>的私有数据

p=p->next;

}

}

/*************************************************

*反向遍历,以链表中每个元素作为参数调用visit函数

*************************************************/

template<class T>

void LinkedList<T>::TraverseBack(void (*visit)(T&element))

{

Node* p=head->prior;

while(p!=head)

{

visit(p->data);//注意此时外部visit函数有权限修改LinkedList<T>的私有数据

p=p->prior;

}

}

/**************************************************

*返回链表的元素引用,并可读写.实际上链表没有[]意义上的所有功能

*因此[]函数是有限制的.重载它是为了客户端代码简洁,因为从链表读写

*数据是最常用的

***************************************************/

template<class T>

T& LinkedList<T>::operator [](int index)

{

assert(index>0 && index<=elementToatal() && !isEmpty());//[]函数使用前提条件

Node* p=head->next;

while(--index) p=p->next;

return p->data;

}

/***************************

*清空链表

***************************/

template<class T>

void LinkedList<T>::clearAllElement()

{

Node* p=head->next,*pTemp=0;

while(p!=head)

{

pTemp=p->next;

delete p;

p=pTemp;

}

head->prior=head->next=head;//收尾工作

}

/******************************

*析构函数,若内存足够没必要调用该函数

*******************************/

template<class T>

LinkedList<T>::~LinkedList()

{

if(head)//防止用户显式析构后,对象又刚好超出作用域再调用该函数

{

clearAllElement();

delete head;

head=0;

}

}

posted @ 2013-10-15 17:15  常想一二,不思八九  阅读(548)  评论(0)    收藏  举报