【数据结构】数据结构与算法基础 课程笔记 第二章 线性表

一、线性表的定义和特点

1.定义

  • 线性表(Linear List)是由n(n>=0)个数据特性相同的元素(结点)a1、a2、a3、...、an组成的有限序列

  • 其中数据元素的个数n定义为表的长度;

  • 当n=0时称为空表;

  • 将非空的线性表记作:(a1,a2,a3,...,an);

  • 线性表是一种典型的线性结构。

2.图示

二、案例引入

  1. 一元多项式的运算

2.稀疏多项式的运算

三、线性表的类型定义

四、顺序表:线性表的顺序表示和实现

顺序表的定义

  • 线性表的顺序表示又称为顺序存储结构顺序映像;一般称为顺序表(Sequential List)

  • 顺序存储即是逻辑上相邻的数据元素,物理上也相邻,因此任一元素均可随机存取;

  • 顺序表的第一个数据元素的存储位置,称作顺序表的起始位置基地址

  • 顺序表包含两个部分,一个存储数据的数组和一个存储数据数量的整型量。

顺序表的操作

顺序表的准备


#include<iostream>
#define MAXSIZE 100
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2//预先对带有特殊意义的返回值进行赋值
typedef int Status;//函数返回值的数据类型用Status表示,易于更改
typedef char ElemType;//用ElemType表示数据元素的数据类型
struct SqList{
    int *elem;//动态分配数组的基地址为elem,也就是顺序表的基地址
    int length;//顺序表的长度
};//定义结构体SqList作为顺序表
SqList L;//定义了一个SqList类型的变量L,对其中数据的引用方式和结构体相同

顺序表初始化


Status InitList_Sq(SqList &L){
    L.elem=new ElemType[MAXSIZE];//新开辟一片单位长度为数据类型ElemType长度、能存储数据数为MAXSIZE的连续的存储空间,其首地址为elem
    if(!L.elem) exit(OVERFLOW);//如果上面开辟失败,则L.elem为0,取反则if条件成立,停止当前进程并返回OVERFLOW
    L.length=0;
    return OK;
}

销毁顺序表


void DestroyList(SqList &L){
    if(L.elem) delete L.elem;//如果线性表存在首地址,就表明线性表存在,此时销毁其首地址,则线性表一并销毁
}//动态分配顺序表内存的好处就在这里,当不需顺序表时,就可以使用销毁函数即使释放占用的存储空间。

清空顺序表


void ClearList(SqList &L){
    L.length=0;//若线性表长度为0,就表明线性表中没有数据,每个元素位置可随意赋值
}

求顺序表的长度


int GetLength(SqList L){
    return(L.length);
}

判断顺序表是否为空


int IsEmpty(SqList L){
    if(L.length==0) return 1;
    else return 0;
}

获取指定位置数据元素


int GetElem(SqList L,int i,ElemType &e){
    if(i<1||i>L.length) return ERROR;
    e=L.elem[i-1];
    return OK;
}

查找与指定值相同的数据元素的位置


int LocateElem(SqList L,ElemType e){
    for(int i=0;i<L.length;i++)
        if(L.elem[i]==e) return i+1;
    return 0;
}

顺序表的插入


Status ListInsert_Sq(SqList &L,int i,ElemType e){//i是要插入元素的位置,e是要插入的元素
    if(i<1||i>L.length+1) return ERROR;//元素位置越界,返回错误
    if(L.length==MAXSIZE) return ERROR;//顺序表满了,返回错误
    for(int j=L.length-1;j>i-1;j--)//从最后一个元素开始逐一往后挪
        L.elem[j+1]=L.elem[j];
    L.elem[i-1]=e;
    L.length++;
    return OK;
}

顺序表的删除


Status ListDelete_Sq(SqList &L,int i){//删除顺序表中的第i个元素
    if(i<1||i>L.length) return ERROR;
    if(!L.length) return ERROR;
    for(j=i;j<=L.length-1;j++){//从要删除的元素的下一个开始逐一往前挪
        L.elem[j-1]=L.elem[j];
        L.length--;
    }
    return OK;
}

顺序表操作的算法分析

查找算法分析

  • 平均查找长度 ASL (Average Search Length):为确定被查找数在表中的位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度。其反映了算法的时间复杂度。

  • 顺序表查找算法的时间复杂度为o(n).

插入算法分析

删除算法分析

顺序表总结

随机存取法

在访问顺序表时,可以快速地计算出任何一个数据元素的存储地址,因此可以粗略地认为,访问每个元素所花时间相等。这种存取元素的方法称为随机存取法其实就是直接存取任一元素,不用逐一访问

顺序表的操作算法分析

  • 时间复杂度:查找、插入、删除算法的平均时间复杂度为o(n)。

  • 空间复杂度:由于顺序表没有占用辅助空间(使用其他数组),因此其空间复杂度S(n)=o(1)。

顺序表的优缺点

优点
  • 存储密度大;

  • 可以随机存取表中任一元素,也就是直接存取;

缺点
  • 在插入或删除某一元素时,需要移动大量元素;

  • 浪费存储空间;

  • 属于静态存储形式,数据元素的个数不能自由扩充。

五、链表:线性表的链式表示和实现

链表的基本概念

线性表的链式存储结构

  • 线性表中数据元素(节点)在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理位置上不一定相邻。

  • 线性表的链式表示又称为非顺序映像或链式映像

结点

定义
  • 数据元素的存储映像,由数据域和指针域(又称为链域)两部分组成;

  • 数据域存储的是元素数值数据,指针域存储的是该结点直接后继的存储位置,即地址。

概念辨析
  • 头指针:是指向链表中第一个结点的指针。

  • 首元结点:是指链表中存储第一个数据元素的结点。

  • 头结点:是在链表的首元结点之前附设的一个结点。

头结点的相关概念和作用
  • 空表的表示:无头结点时,头指针为空时表示空表;有头结点时,当头结点的指针域为空时表示空表。

  • 在链表中设置头结点的好处:

头结点的数据域可以为空,也可以存放线性表长度等附加信息,但头结点不能计入链表长度值

链表

定义
  • 链表由头指针和被指针链串起来的若干结点组成;

  • 链表是顺序存取的

分类
  • 单链表:又称线性链表,结点只有一个指针域,由于其由头指针唯一确定,所以单链表可以由头指针的名字来命名。

  • 双链表:结点有一前一后两个指针域。

  • 循环链表:首尾相接。

单链表的操作

单链表的准备


typedef struct Lnode{//定义一个结构体,并为其起一个别名 Lnode,即为链表结点的数据类型
    ElemType data;//数据域中存储的数据;若数据为类似学生成绩管理系统的一组数据,则可先预先定义结构体ElemType
    struct Lnode *next;//定义一个指向struct Lnode类型的指针next
}Lnode,*LinkList;// 这里在定义结构体后直接定义一个指针类型LinkList,指向结构体Lnode;这样便于下面定义指向结点的指针变量

struct Lnode{//也可以这样定义,更方便理解
    ElemType data;
    struct Lnode *next;
};
typedef Lnode *LinkList;

Lnode *L;//定义头指针L,也可以用于定义某个结点的指针;定义了头指针就相当于定义了单链表L
LinkList L;//直接用指针类型LinkList来定义头结点L,一般就用LinkList来定义链表,一目了然

单链表初始化


Status InitList(LinkList &L){
    L=new Lnode;//法一,用new开辟一片长度与Lnode相同的内存,并将其地址赋值给L
    L=(LinkList)malloc(sizeof(Lnode));//法二,用malloc函数开辟出长度为Lnode的内存,转化为LinkList型的指针后赋值给L
    L->next=NULL;//将L指向的结点的指针域清空
    return OK;
}

判断单链表是否为空


int ListEmpty(LinkList L){
    if(L->next)//通过判断头指针指向的头结点的指针域是否为空来判断单链表是否为空
        return 0;
    else
        return 1;
}

销毁单链表


Status DestroyList(LinkList &L){//从头结点开始依次释放结点空间
    LinkList p;//定义一个指针变量p用来暂时存放要销毁的指针
    while(L){//当L不为空则条件成立,循环继续进行,销毁此时L所指向的结点
        p=L;//p指向L当前指向的结点
        L=L->next;//L指向下一个结点
        delete p;//释放p所指向结点的空间,也就是销毁这个结点
    }
    return OK;
}

清空链表


Status ClearList(LinkList &L){//将L重置为空表
    LinkList p,q;
    p=L->next;
    while(p){
        q=p->next;//让q指向第二个含有元素的结点
        delete p;//释放首元结点的空间,即为销毁首元结点
        p=q;//将指向第二个结点的指针再赋给p
    }//到最后所有含有元素的结点都会被清空
    L->next=NULL;//将头结点的指针域清空
    return OK;
}

求单链表的表长


int ListLength_L(LinkList L){
    LinkList p;//设置一个工具指针来遍历
    p=L->next;//p指向首元结点
    int i=0;
    while(p){
        i++;
        p=p->next;
    }
    return i;
}

获取指定位置数据元素


Status GetElem_L(LinkList L,int i,ElemType &e){//获取第i个元素的内容,通过变量e返回
    LinkList p;
    while(p&&j<i){//用变量j来计数
        p=p->next;//设置一个工具指针p来遍历
        j++;//到最后j和i相等时,会触发循环结束条件
    }//数到第i个结点为止
    if(!p||j>i) return ERROR;//如果指向第i个结点的指针为空或j比i大,则返回错误
    e=p->data;//将p指向的结点的内容赋值给e
    return OK;
}

根据指定数据获取该数据所在的位置


Lnode *LocateElem_L(LinkList L,ElemType e){//根据e找到其所在结点并返回结点的地址,即指针
    p=L->next;
    while(p&&p->data!=e)//查找成功返回p,查找失败则p为NULL
        p=p->next;
    return p;
}

根据指定数据获取该数据位置序号


int LocateElem_L(LinkList L,ElemType e){//在链表L中查找值为e的数据元素的位置序号
    p=L->next;
    j=1;
    while(p&&p->data!=e){
        p=p->next;
        j++;
    }
    if(p) return j;
    else return 0;
}

单链表的插入


Status ListInsert_L(LinkList &L,int i,ElemType e){//在L中第i个元素之前插入数据元素e
    p=L;//p指向头结点
    j=0;
    while(p&&j<i-1){
        p=p->next;//指针从首元结点开始向后移动
        j++;
    }//当p移动到第i个结点之前的位置停下
    if(!p||j>i-1) return ERROR;
    s=new Lnode;
    s->data=e;//新生成一个结点s,数据域里存放要插入的元素
    s->next=p->next;//s指向的结点的指针域里存放要插入的位置的下一个结点的地址
    p->next=s;//用s的地址取代下一个结点在p上的地址
    return OK;
}

单链表的删除


Status ListDelete_L(LinkList &L,int i,ElemType &e){//将单链表中第i个元素删除
    p=L;
    j=0;
    while(p&&j<i-1){
        p=p->next;
        j++;
    }//循环后p指向第i个结点的前驱
    if(!(p->next)||j>i-1) return ERROR;
    q=p->next;//q指向第i个结点,也就是要删除的结点
    p->next=q->next;//第i-1个结点指针域里的指针指向第i+1个结点
    e=q->data;//将第i个结点的数据赋值给e来返回
    delete q;//释放q所指向的结点,也就是第i个结点的存储空间,也即是删除该结点
    return OK;
}

单链表的查找、插入、删除算法的时间效率:

综上,单链表的查找、插入和删除算法的时间复杂度都为O(n)。

下面两种插入算法的时间复杂度也是O(n)。

建立单链表——头插法


void CreatList_H(LinkList &L,int n){//每一个新结点都插在头结点后
    L=new Lnode;
    L->next=NULL;//先建立一个单链表L
    for(i=n;i>0;i--){ 
        p=new Lnode;//建立一个指向新结点的指针p
        cin>>p->data;//输入p指向的结点的元素值
        p->next=L->next;//p的下一个结点是原先头结点的下一个结点
        L->next=p;//头结点的下一个结点是p
    }
}

建立单链表——尾插法


void CreatList_H(LinkList &L,int n){//每一个新结点都插到表尾
    L=new Lnode;
    L->next=NULL;//建立一个单链表L
    r=new Lnode;
    r=L;//建立尾指针r,和L一样指向头结点
    for(i=0;i<n;i++){
        p=new Lnode;//新增一个结点,建立指向新结点的指针p
        cin>>p->data;//输入新结点的元素值
        p->next=NULL;//因为是尾插,所以将新结点指针域设为空
        r->next=p;//将新结点的地址赋给尾指针指向的结点的指针域
        r=p;//r指向新的尾结点
    }
}

循环链表

特点

  • 最后一个结点的指针域指向头结点,整个链表形成一个环;

  • 从循环链表的任意一个结点出发,都可以找到其他任意结点;

  • 对循环链表进行遍历操作时,终止条件是一个结点的指针域为头指针。

带尾指针循环链表的合并


LinkList Connect(LinkList Ta,LinkList Tb){//Ta、Tb都是尾指针,表示非空的单循环链表
p=Ta->next;//p指向a表的头结点
Ta->next=Tb->next->next;//a表的最后一个结点连上b的首元结点
delete Tb->next;//释放Tb表的头结点
Tb->next=p;//p是Ta表的头结点,这样Tb的尾结点就和Ta的头结点相连了

双向链表

意义

  • 由于链表是顺序存取的,所以只能向后查询而不能向前查询,查询某结点后继的执行时间为O(1),而查询其前驱的时间为O(n),因此定义了双向链表来克服这一缺点。

  • 在单链表的每个结点里再增加一个其直接前驱的指针域prior,这样就变成了双向链表。

  • 由于双向链表的特殊结构,故其具有对称性


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

双向链表的操作

双向链表的结构定义

typedef struct DulNode{
    Elemtype data;
    struct DulNode *prior,*next;
}DulNode,*DuLinkList;
双向链表的插入

void ListInsert_Dul(DuLinkList &L,int i,Elemtype e){//在带头结点的双向循环链表L中的第i个位置之前插入元素e
    if(!(p=GetElemP_Dul(L,i))) return ERROR;
    s=new DulNode;//给指向新结点的新指针s开出一块地
    s->date=e;//让新指针s指向元素为e的新结点
    s->prior=p->prior;//让p的前驱变成s的前驱
    p->prior->next=s;//让s成为p前驱的后继
    s->next=p;//让p成为s的后继
    p->prior=s;//让s成为p的前驱
    return OK;
}
双向链表的删除

void ListDelete_Dul(DuLink &L,int i,ElemType &e){//删除带头结点的双向循环链表L的第i个元素,并用e返回
    if(!(p=GetElemP_Dul(L,i))) return ERROR;
    e=p->data;
    p->next->prior=p->prior;
    p->prior->next=p->next;
    free(p);
    return OK;
}

三种链表查找结点的时间效率比较

六、线性表的应用

线性表的合并
  • 问题描述:利用两个线性表La、Lb来表示集合A、B,现要求一个新集合A,作为原来的A和B的交集。

  • 算法步骤:依次取出Lb中的每个元素,在La中查找该元素,若找不到,则将其插入La的最后。


void union(List &La,List Lb){
    La_len=ListLength(La);
    Lb_len=ListLength(Lb);
    for(i=1;i<=Lb_len;i++){
        GetElem(Lb,i,e);
        if(!LocateElem(La,e)) ListInsert(&La,++La_len,e);
    }
}
有序表的合并
  • 问题描述:已知线性表La和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且其仍有序排列。

  • 算法步骤:

  1. 创建一个空表Lc;

  1. 分别对La和Lb表摘取最小的数据元素进行比较,其中较小的放入Lc,直至La或Lb为空,这时把非空的表直接续在Lc之后即可。

用顺序表来实现

void MergeList_Sq(SqList LA,SqList LB,SqList &LC){
    pa=LA.elem;
    pb=LB.elem;//pa和pb分别指向LA、LB的基地址,也就是各自最小的元素的地址
    LC.length=LA.length+LB.length;
    LC.elem=new ElemType[LC.length];//为新表分配一个数组空间
    pc=LC.elem;//pc指向新表的第一个元素的地址,即基地址
    pa_last=LA.elem+LA.length-1;
    pb_last=LB.elem+LB.length-1;//pa_last和pb_last分别指向LA、LB的最后一个元素的地址
    while(pa<=pa_last&&pb<=pb_last){//两个表都非空,若空,则pa>pa_last
        if(*pa<=*pb) *pc++=*pa++;
        else *pc++=*pb++;
    }
    while(pa<=pa_last) *pc++=*pa++;//若LB已到达表尾,则将LA中剩余元素加入LC
    while(pb<=pb_last) *pc++=*pb++;
}
用链表来实现

void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){
    pa=La->next;
    pb=Lb->next;
    pc=Lc=La;//用La的头结点作为Lc的头结点
    while(pa&&pb){
        if(pa->data<=pb->data){
            pc->next=pa;
            pc=pa;
            pa=pa->next;
        }
        else{
            pc->next=pb;
            pc=pb;
            pb=pb->next;
        }
    pc->next=pa?pa:pb;
    delete Lb;
}

教材课后习题及答案

http://t.csdn.cn/rhaF4

(11) 创建一个包括 n 个结点的有序单链表的时间复杂度是() 。

A . O(1) B . O(n) C. O(n 2) D . O(nlog 2n)

答案: C
解释:单链表创建的时间复杂度是 O(n) ,而要建立一个有序的单链表,则每生成一个新结点时需要和已有的结点进行比较,确定合适的插入位置,所以时间复杂度是O(n2) 。

期末题库

  • 判断题:在以HL为表头指针的带头结点的单链表和循环单链表中,链表为空的条件分别为HL->next==NULL 和 HL==HL->next 。

答案:Y
解释:循环单链表为空时,尾结点的指针指向头结点,且头结点中就存放着尾指针。
  • 线性表采用链表存储时,结点和结点内部的存储空间可以是不连续的。

答案:Y
  • 设一个链表最常用的操作是在末尾插入结点和删除尾结点,则选用( )最节省时间。

A. 单链表 B. 单循环链表 C. 带尾指针的单循环链表 D. 带头结点的双循环链表

答案:D
解释:若选C,则删除尾结点需要遍历整个链表,耗时过多,选D则时间复杂度仅为O(1)
  • 若某线性表中最常用的操作是在最后一个元素之后插入一个元素和删除第一个元素,则采用( )存储方式最节省运算时间。

A. 单链表 B. 双链表 C. 仅有尾指针的单循环链表 D. 仅有头指针的单循环链表

答案:C
解释:题目中提到删除第一个元素,此时使用具有尾指针的单链表最高效。
  • 查找线性表第i个元素的时间同i的大小有关系吗?

不一定。
线性表在链式存储时,查找第i个元素的时间同i的值成正比;
而在顺序存储时,查找第i个元素的时间与i的值无关。
  • 使用双向链表存储数据,其主要优点是?

提高检索速度。无论是带头指针的单链表还是带尾指针的单链表,检索时都可能需要遍历整个链表。
  • 在单链表中,任何两个元素的存储位置之间都有固定的联系,因为可以从头结点进行查找任何一个元素.

Y
  • 顺序表是用一维数组实现的线性表,数组的下标可以看成是元素的绝对地址。

Y
  • 循环单链表的最大优点是什么?

答案:从任一结点出发都能访问到链表中的每一个元素。

PTA

  • 判断题:将N个数据按照从小到大顺序组织存放在一个单向链表中。如果采用二分查找,那么查找的平均时间复杂度是O(logN)。

答案:False
分析:二分查找是不可以用链表存储的,二分查找需要借助下标实现对一个递增或递减序列的折半查找,只能通过数组实现。
  • 填空题:逆转单链表


List Reverse( List L )//逆转单链表L的函数
{
    Position Old_head, New_head, Temp;
    New_head = NULL;//New指向空,之后将原表的元素从头逐一用头插法加进去
    Old_head = L->Next;//Old指向首元结点,方便接下来用循环语句进行遍历

    while ( Old_head )  {
        Temp = Old_head->Next;//Temp指向当前的第二个带值结点,有暂存的作用
        Old_head->Next = New_head;//将Old指向结点的指针连到New指向的地方
        New_head = Old_head;//New指针连到Old指向的结点  
        Old_head = Temp;//Old指针向后移动一位 
    }
    
    L->Next = New_head;//最后将L的头结点指针连到New指向的结点,完成逆转
    return L;
}
  • 顺序表和链表的比较

空间性能的比较:

存储空间的分配:

顺序表需要提前分配存储空间,容易造成空间浪费和空间溢出;

链表不需要预先分配,存储的元素个数没有限制。

存储密度的大小:

顺序表的存储密度大,空间利用率高,链表的存储密度小,空间利用率低。

时间性能的比较:

存取元素的效率:

顺序表存取元素操作的时间复杂度为O(1),链表为O(n).

插入删除操作的效率:

链表为O(1),顺序表为O(n)。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值