第2章 线性表

来源:互联网 发布:亚像素角点检测算法 编辑:程序博客网 时间:2024/05/18 01:40

【考纲内容】

(一)线性表的定义和基本操作

(二)线性表的实现

  1. 顺序存储
  2. 链式存储
  3. 线性表的应用

【知识框架】

这里写图片描述

线性表的定义和基本操作

线性表的定义

线性表相同数据类型的n(n>=0)个数据元素的有限序列。其中n为表长,当n=0时,该线性表是一个空表,若用L命名线性表,则其一般表示如下:

L=(a 1 ,a 2 ,a 3 ,,a i ,a i+1 ,,a n )

除第一个元素外,每一个元素都有一个直接前驱,除最后一个元素都有一个直接后继,这种线性有序的逻辑结构正是线性表名字的由来。

由此总结线性表的特点:

  • 表中元素个数有限
  • 表中元素具有逻辑上的顺序性,在序列中各元素排序有其先后顺序。
  • 表中元素都是数据元素,每一个元素都是单个元素。
  • 表中元素数据类型相同。这意味着每个元素占有相同大小的存储空间。
  • 表中元素具有抽象性。即仅考虑元素间的逻辑关系,不考虑元素究竟表示什么内容。

注意,线性表是一种逻辑结构,表示元素间一对一的相邻关系。顺序表和链表是具体的存储结构,两者是属于不同层面上的概念,因此不要混淆。

线性表的操作

一个数据结构的基本操作是指其最核心、最基本的操作。其他复杂的操作可以通过调用基本的操作来实现。线性表的基本操作如下:

  • InitList(&L):初始化表。构造一个空的表。
  • Length(L):求表长。返回表的长度,即表中数据元素的个数。
  • LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值得元素e。
  • GetEle(L,i):按位查找操作。获取表L中第i个位置元素的值。
  • ListInsert(&L,i,e):插入操作。在表L中第i个位置插入元素e。
  • ListDelte(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
  • PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
  • Empty(L):判空操作。若L为空表,则返回true,否则返回false。
  • DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占的内存空间。

注意:①基本操作的实现取决于采用哪一种存储机构,存储结构不同,算法的实现也不同。②‘&’是C++中的引用。如果传入的变量是指针型的变量,且在函数体内要对传入的指针进行改变,则将用到指针变量的引用型。在C中采用指针的指针也可达到相同的效果。③函数传地址和传值取决于是否要对传入前的数据进行有改变性的操作(插入、删除、销毁、初始化)或者是否需要进行返回,例如ListDelete(&L,i,&e),对原本的L进行删除操作,并且需要返回e的值给调用函数,所以e的类型也是引用型。

线性表的顺序表示

顺序表的定义

线性表的顺序存储又称为顺序表,它是用一组地址连续的存储单元,依次存储线性表中的数据元素,从而使得逻辑上相邻的元素在物理上也相邻。

这里写图片描述

注意:线性表中元素的位序是从1开始的,而数组中元素的下标是从0开始的。

假定线性表的元素类型为ElemType,线性表的顺序存储类型描述为

//静态#define MaxSize 50//定义线性表的最大长度typedef struct{    ElemType data[MaxSize];//顺序表的元素    int length;//顺序表的当前长度}SqList;//顺序表的类型定义

一维数组可以是静态分配的,也可以是动态分配的。在静态分配时,由于数组的大小和空间都已事先固定,一旦空间占满,在加入新的数据将产生溢出,就会导致程序崩溃;而在动态分配时,存储数组的空间是在执行程序的过程中通过动态存储分配语句分配的,一旦数据空间占满,可以开辟另外一块更大的存储空间,用来替换原来的存储空间,从而达到扩充存储数组空间的目的,而不需要一次性地划分所有所需空间给线性表。

//动态#define InitSize 100//表长度的初始定义typedef struct{    ElemType *data;//指示动态分配数组的指针    int MaxSize,length;//数组的最大容量和当前个数}SqList;//动态分配数组顺序表的类型定义

C的初始动态分配语句为:

L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);

C++的初始动态分配语句:

L.data=new ElemType[InitSize];

注意:动态分配并不是链式存储,同样还是属于顺序存储结构,其物理结构没有变化,依然是随机存取方式,只是分配的空间大小可以在运行时决定。

顺序表的特点:

  • 随机存取,通过首地址和元素序号可以在O(1)的时间内找到指定的元素。
  • 存储密度高,每个节点之存储元素。
  • 插入删除需要移动大量元素。

注意:顺序存取是一种读写方式,不是存储方式,有别于顺序存储。

线性表的链式表示

单链表

单链表是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立起数据元素之间的线性关系,对每个链表结点,除了存放自身的信息之外,还需要存储一个指向其后继的指针

单链表中结点类型描述如下:

typedef struct LNode{//定义单链表结点类型    ElemType data;//数据域    struct LNode *node;//指针域}LNode, *LinkList;

通常采用头指针来标识一个单链表,如单链表L,头指针为“NULL”表示一个空表,为了操作上的方便,在单链表的第一个结点之前附加一个结点,称为头结点,头结点的数据域可以不设任何信息,也可记录表长等相关信息。头结点的指针域指向线性表的第一个元素结点,如图所示。
这里写图片描述

头结点头指针的区别:不管带不带头结点,头指针始终指向链表的第一个结点,而头结点是带头结点链表中的第一个结点,结点内通常不存储信息。

引入头结点的两个优点:

  • 由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表中的其他位置上的操作一致,无需进行特殊处理。
  • 无论链表是否为空,其头指针是指向头结点的非空指针,因此空表和非空表的处理也就统一了。

建立链表

头插法

这里写图片描述

算法:

typedef struct LNode{//定义单链表结点类型    ElemType data;//数据域    struct LNode *node;//指针域}LNode, *LinkList;//从表尾到表头逆向建立单链表L,每次均在头结点之后插入元素LinkList CreateList1(LinkList &L){    LNode *s;int x;    L=(LinkList)malloc(sizeof(LNode));//创建头结点    L->next=NULL;//初始化为空链表    scanf("%d",&x);//输入结点的值    while(x!=9999){//输入9999表示结束        s=(LNode*)malloc(sizeof(LNode));//创建新结点        s->data=x;        s->next=L->next;        L-next=s;//将新结点插入表中,L为头指针        scanf("%d",&x);    }//while结束    return L;}

采用头插法建立单链表,读入数据与生成的链表中的元素的顺序是相反的。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。

尾插法

该方法是将新结点插入到当前链表的表尾,为此需增加一个尾指针r,使其始终指向当前链表的尾结点。

这里写图片描述

typedef struct LNode{//定义单链表结点类型    ElemType data;//数据域    struct LNode *node;//指针域}LNode, *LinkList;//从表头到表尾正向建立单链表L,每次均在表尾插入元素LinkList CreateList2(LinkList &L){    int x;    L=(LinkList)malloc(sizeof(LNode));    LNode *s,*r=L;//r表示尾指针    scanf("%d",&x);//输入结点的值    while(x!=9999){        s=(LNode *)malloc(sizeof(LNode));        s->data=x;        r->next=s;        r=s;//r指向新的表尾指针        scanf("%d",&x);    }    r->next=null;//尾结点指针置空    return L;}

注意尾结点的指针域最后一定要置空。

查找链表

这里分为两种,按值查找按序号查找,比较简单

插入结点

比较简单,注意:先判断插入位置的合法性,然后找到前驱结点,再插入。

删除结点

也是要先判断删除位置的合法性,然后找到前驱,再删除,删除要调用free(q)来释放结点的存储空间。

求表长

注意,带头结点的单链表表长不包括头结点,长度是数据结点的个数

双链表

为了克服单链表访问前驱结点时间复杂度为O(n)的问题,引入双链表,双链表的结点中有两个指针priornext,分别指向其前驱结点和后继结点。

这里写图片描述

双链表结点类型定义如下:

typedef struct DNode{//定义双链表结点类型    ElemType data;//数据域    struct DNode *prior,*next;//前驱和后继指针}DNode, *Linklist;

双链表的插入操作

//在双链表中p所指的结点之后插入结点*s①s->next=p->next;②p->next->prior=s;③s-prior=p;④p->next=s;

这里写图片描述

双链表的删除操作

p->next=q->next;q->next->prior=p;free(q);//释放结点空间

这里写图片描述

循环链表

循环单链表

循环单链表与单链表的区别在于,表中的最后一个结点的指针不是NULL,而改为指向头结点,从而整个链表形成一个环。

这里写图片描述

注意:

  • 循环单链表中,表尾结点*r的next域指向L,故表中没有指针域为NULL的结点,因此循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针。

  • 在单链表中,为了操作方便,我们设了一个头指针,才能对其进行各种操作(遍历,插入等),尾插法中我们还设了一个尾指针,而在循环链表中我们可以仅仅只设一个尾指针,就能高效操作,因为尾指针的指针域指向头指针。

循环双链表

相比循环单链表,循环单链表的头结点的prior指针还要指向表尾结点。

这里写图片描述

注意:带头结点的循环双链表的判空条件为头结点的priornext都指向自身。而不是都为NULL

静态链表

静态链表是借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,这里的指针是指结点的数组下标,又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。

这里写图片描述

静态链表结构类型的描述如下:

#define MaxSize 50//静态链表的最大长度typedef struct{    ElemType data;//数据域    int next;//指针域,指向下一个元素的下标} SLinkList[MaxSize];

静态链表以next==1作为结束标志。在不支持指针的高级语言中,这是一种非常巧妙的方法。

顺序表和链表的比较

1.存储方式

顺序表随机存取,链表只能顺序存取。

2.逻辑结构和物理结构

采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也相邻。

采用链式存储时,逻辑上相邻的元素,其对应的物理存储位置不一定相邻,其对应的逻辑关系是通过指针来表示的。

注意:区分存储方式和存取方式。

3.查找、插入和删除操作

对于按值查找,当顺序表在无序的情况下,两者的时间复杂度都是O(n),而当顺序表有序时,可采用折半查找,时间复杂度为O(log~2~n)。

对于按序号查找,顺序表支持随机访问,时间复杂度为O(1),而链表的时间复杂度为O(n)

顺序表的删除、插入操作,平均要移动半个表的元素;链表的插入、删除只需要修改相应的指针域即可。

4.空间分配

顺序存储的静态存储,预先分配空间过大,可能导致大量内存限制,过小可能导致内存溢出。

顺序存储的动态存储,虽然存储空间可以扩充,但需要移动大量元素,而且内存中如果没有更大的连续内存空间将导致分配失败。

链式存储的结点空间在需要的时候动态分配,操作灵活,高效。

题型归类

顺序表

  • 删除元素 P18 3、4、5、6(包含去掉某一元素、介于两值之间的数、重复的数)
  • 元素逆置 P18 2、
  • 归并顺序表 P18 7
  • 综合 P18 8、9

注意:题目给出的是有序顺序表还是普通顺序表。

链表

  • 删除元素 P37 1、4、7
  • 元素逆置 P37 5、
  • 排序 P37 6、9
  • 综合 P37 8、14、15

注意:题目给出的是带头结点的还是不带头结点的,有些题需要额外的防断链结点前驱结点,稍复杂题目可以考虑递归。

思维扩展 P57

归纳总结

本章是算法设计重点考察章,题型分两大类顺序表链式表

顺序表一般结合排序和查找的几种算法

链表常用思路是:头插法尾插法逆置法归并法双指针.

原创粉丝点击