Loading

数据结构笔记二:线性表

线性表

线性表的定义

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

\[L(a_1,a_2,...,a_i,a_{i+1},...,a_n) \]

\(a_i\)是线性表中的“第i个”元素线性表中的位序(位序从1开始,数组下标从0开始)

\(a_1\)是表头元素;\(a_n\)是表尾元素

除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。

线性表的基本操作

InitList(&L ):         //初始化表。构造一个空的线性表L,分配内存空间
DestoryList(&L ):      //销毁操作。销毁线性表,并释放线性表L所占用的内容空间。

ListInsert(&L,i,e):	   //插入操作。在表L中的第i个位置上指定元素e
ListDelete(&L,i,&e);   //删除操作。删除表中L中第i个位置的元素,并用e返回删除元素的值

LocateElem(L,e);	   //按值查找操作。在表L中查找具有给定关键字的元素。
GetElem(L,i);		   //按位查找操作。获取表L中第i个位置的元素的值。

Length(L);			   //求表长。返回线性表L的长度,即L中数据元素的个数
PrintList();            //输出操作。按前后顺序输出线性表L的所有元素值
Empty(L);			  //判空操作。若L为空表,则返回true,否则返回false;

线性表的顺序表示

顺序表的定义

顺序表——用顺序存储的方式实现线性表。

image-20210807204113026

//定义
//静态分配
#define MaxSize 10			//定义最大长度
typedef struct{
    Elemtype data[Maxsize];	 //顺序表的元素
    int length;	             //顺序表的当前长度 
}Sqlist;

//动态分配
#define InitSize  100		//表长度的初始定义
typedef struct{
    Elemtype *data;	 		//指示动态分配数组的知识
    int MaxSize,length;	     //数组的最大容量和当前个数
}SeqList;


//c动态分配语句
L.data=(Elemtype*)malloc(sizeof(Elemtype)*InitSize);

//c++动态分配语句
L.data=new Elemtype[InitSize];

顺序表的特定:

  1. 随机访问,即可以在\(O(1)\)s时间内找到第i个元素
  2. 存储密度高,每个节点只存储数据元素
  3. 拓展容量不方便(基表采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
  4. 插入,删除操作不方便,需要移动大量元素

顺序表上的基本操作的实现

插入操作

//静态分配下
bool ListInsert(SqList &L,int i,Elemtype e)
{
    if(i<1||i>L.length+1)			//判定i的范围是否有效
        return false;
    if(L.length>=MaxSize)			//当前存储空间已满,不能插入
        return false;
    for(int j=L.length;j>=i;--j)	//后移
        L.data[j]=L.data[j-1];
    L.data[i-1]=e;				
    L.length++;
    return true;
}

最好情况:在表尾插入(即\(i=n+1\)),循环0次;最好时间复杂度为\(O(1)\)

最坏情况:在表头插入(即\(i=1\)),循环n次;最好时间复杂度为\(O(n)\)

平均情况:假设\(p_i(p_i=1/(n+1))\)是在第\(i\)个位置上插入一个结点的概率,则移动结点的平均次数为

\[\sum^{n+1}_{i=1}p_i(n-i+1)=\frac {1}{n+1} \sum^{n+1}_{i=1}(n-i+1)=\frac {1}{n+1}\frac {n(n+1)}{2}=\frac {n}{2} \]

插入算法的平均时间复杂度为\(O(n)\)

删除操作

//静态分配下
bool ListDelete(SqList &L,int i,Elemtype& e)
{
    if(i<1||i>L.length+1)			//判定i的范围是否有效
        return false;
    e=L.data[i-1];					//赋值给e
    for(int j=i;j<L.length;j++)
       L.data[j-1]=L.data[j];
    L.length--;
    return true;
}

最好情况:删除表尾元素(即\(i=n\)​),无需移动元素;最好时间复杂度为\(O(1)\)​​

最坏情况:删除表头元素(即\(i=1\)),需移动除表头元素外的所有元素;最好时间复杂度为\(O(n)\)

平均情况:假设\(p_i(p_i=1/n)\)​是在第\(i\)​​个位置上删除一个结点的概率,则移动结点的平均次数为

\[\sum^{n}_{i=1}p_i(n-i)=\frac {1}{n} \sum^{n}_{i=1}(n-i)=\frac {1}{n}\frac {n(n-1)}{2}=\frac {n-1}{2} \]

删除算法的平均时间复杂度为\(O(n)\)

查找操作(按值查找)

int LocateELem(SqList L,Elemtype e)
{
    int i=0;
    for(i=0;i<L.Length;++i)
        if(L.data[i]==0)
            return i+1;	·		 //下标为i的元素值等于e,返回其位序i+1
    return 0;					//退出循环,说明查找识别
}

最好情况:查找的元素就在表头,仅需比较一次;最好时间复杂度为\(O(1)\)

最坏情况:查找的元素在表尾(或不存在)时,需比较n次;最好时间复杂度为\(O(n)\)

平均情况:假设\(p_i(p_i=1/n)\)是查找的元素在第\(i\)​个位置的概率,则移动结点的平均次数为

\[\sum^{n}_{i=1}p_i\times i=\frac {1}{n} \sum^{n}_{i=1}i=\frac {1}{n}\frac {n(n+1)}{2}=\frac {n+1}{2} \]

查找算法的平均时间复杂度为\(O(n)\)

线性表的链式表示

单链表的定义

//单链表中的结点类型
typedef struct Lnode{
    Elemtype data;			//数据域
    struct Lnode* next;		//指针域
}Lnode,*LinkList;

要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点

为了操作上的方便,在单链表第一个结点之前附加一个头结点。头节点的数据域可以不设任何信息。引入头结点后:

  1. 在链表的第一个位置上的操作与其他位置的一致,无须进行特殊处理
  2. 无论链表为空,其头指针都指向头结点的非空指针(空表中头节点的指针域为空),实现空表与非空表的处理统一。

单链表的基本操作

建立操作

头插法

该方法从一个空表开始,生成一个新结点,并将读取到的数据存放到新结点的数据域中,然后讲新结点插入到当前链表的表头。

image-20210811201305880

//带头结点
LinkList List_HeadInsert(LinkList &L)
{
    LNode *s;int x;
    L=(LinkList)malloc(sizeof(LNode));		//创建头结点
    L->next=NULL;						  //初始为空链表
    scanf("%d",&x);						  //输入结点的值
    whille(x!=9999)						  //输入9999表示结点
    {
        s=(LNode*)malloc(sizeof(LNode));
        s-data=x;
        s-next=L->next;					  //将新结点插入表中,L为头结点
        L-next=s;
        scanf("%d",&x);
    }
    return L:
}

平均时间复杂度为\(O(n)\)

头插法应用:链表的逆置

尾插法

image-20210811201321828

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

//带头结点
LinkList List_TailInsert(LinkList &L)
{
    int x;+									
	LNode *s,*r=L;						 //r为表尾指针
    scanf("%d",&x);						  //输入结点的值
    whille(x!=9999)						  //输入9999表示结点
    {
        s=(LNode*)malloc(sizeof(LNode));
        s-data=x;
        r->next=s;
        r=s;							  //r指向新的表尾结点
        scanf("%d",&x);
    }
    r->next=NULL;						   //尾指针置空
    return L:
}

平均时间复杂度为\(O(n)\)

查找操作

按位(序号)查找

LNode *GetElem(LinkList L,int i){    int j=1;				//计数,初始为1    LNode *p=L->next;		 //头结点赋值给p    if(i==0)        return L;			//若i等于0,则返回头结点    if(i<1)        return NULL;		//若i无效,则返回NUll    while(p&&j<i)			//第一个结点开始,查找第i个结点    {        p=p->next;        j++;    }    return p;				//返回第i个结点的指针,若i大于表长,则返回NULL}

平均时间复杂度为\(O(n)\)

按值查找

LNode *LocateElem(LinkList L,ELemtype e){    LNode *p=L->next;    while(p!=NULL&&p->data!=e)		//第一个结点开始查找data域为e的结点        p=p->next;    return p;				//找到后返回该结点指针,否则返回NULL}

平均时间复杂度为\(O(n)\)

插入操作

后插操作:

image-20210811201336312

p=GetElem(L,i-1);				//查找插入位置的前驱结点s->next=p->next;				//第二步p->next=s;					    //第三步

时间复杂度为\(O(n)\)

扩展:对某一结点进行前插操作

s->next=p->next;				//修改指针域,不能颠倒p->next=s;temp=p->data;					//交换数据域部分p->data=s=>data;s->data=temp;

时间复杂度为\(O(1)\)

删除操作

image-20210811201643934

p=GetElem(L,i-1);				//查找删除位置的前驱结点q=p->next;					    //令q指向被删除结点p->next=q->next;				//将*q结点从链中“断开”free(q);					    //释放结点的存储空间

时间复杂度为\(O(n)\)

扩展:删除结点*p

q=p->next;					//令q指向*p的后续结点p->data=p->next->data;		 //和后续结点交换数据域(注意是否为最后一个结点)p->next=q->next;			//将*q结点从链中“断开”free(q);

时间复杂度为\(O(1)\)

双链表的定义

引入前驱指针

image-20210811202846619

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

双链表的基本操作

插入操作

image-20210811202854150

s->next=p->next;			//将结点*s插入到*p之后if(p->next!=NULL)		    //判断是否为最后一个结点    p->next->prior=p;s->next=p;p->next=s;

删除操作

image-20210811202904796

//删除*p后续结点*qp->next=q->next;q->next->priot=p;free(q);

循环链表的定义

循环单链表

表中最后一个结点的指针不是NULL,,而改为指向头结点。

判空条件:是否等于头结点

循环双链表

头结点的prior指针还要指向表尾结点。

image-20210811203619346

当循环双链表为空表是,其头结点和prior域与next域都等于L。

具体操作自己学习实现。

静态链表的定义

静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data与指针域next;

这里的指针是结点的相对地址(数组下标),又称游标。

静态链表要预先分配一块连续的内存空间。

image-20210811204748663

typedef struct{    Elemtype data;			//存储数据元素    int next;				//下一个元素的数组下标}SLinkList[MaxSize];		//定义一个结构体数组

优点:增,删操作不需要大量移动元素

缺点:不能随机存储,只能从头结点开始依次往后查找;容量固定不可变

适用场景:①不支持指针的低级语言 ②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)

顺序表和链表的对比

存储结构

image-20210811205253208

基本操作

image-20210811205541297

image-20210811205803791

image-20210811210004944

image-20210811210112730

选择

image-20210811210339643

posted @ 2021-08-26 17:09  Ligo丶  阅读(803)  评论(0编辑  收藏  举报