数据结构与算法学习笔记-线性表

声明:本博客仅为本人学习途中做的笔记 采自青岛大学王卓老师的视频教学 主要内容为算法思路,具体代码实现还需修改后才能运行,望各位看官多多包涵,您的点赞与评论是对我最大的肯定!


1.线性表的定义和特点

线性表是具有相凤特性的数据元素的一个有限序列
( a 1 , a 2 , . . . a i − 1 , a i , a i + 1 , . . . , a n ) (a_1, a_2, ... a_i-_1, a_i,a_i+_1, ..., a_n) (a1,a2,...ai1,ai,ai+1,...,an) \qquad \qquad \quad \qquad \qquad | \qquad \qquad \qquad | \qquad \qquad \quad |
\qquad \qquad \qquad \qquad 起始结点 \qquad \quad 数据元素 \qquad 终端结点
同一线性表中的元素必定具有相同特性,数据元素之间的关系是线性关系

从以上例子可看出线性表的逻辑特征是:

在非空的线性表,有且仅有一个开始结点a 1 _1 1,它没有直接前趋,而仅有一个直接后继a 2 _2 2; 有且仅有一个终端结点a n _n n ,它没有直接后继,而仅有一个直接前趋a n − 1 _n-_1 n1;
其余的内部结点a i _i i (2 ≤ i _i i ≤ n-1)都有且仅有一个直接前趋a i − 1 _i-_1 i1和一个直接后继a i + 1 _i+_1 i+1

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

2.线性表的顺序表示和实现

顺序存储定义:把逻辑上相邻的数据元素储存在物理上相邻的储存单元中的顺序结构

顺序表(元素)

  • 地址连续
  • 依次存放
  • 随机存取
  • 类型相同

多项式的顺序存储结构类型定义

需要加载头文件:<stdlib.h>

typedef struct {
    
    
    ElemType *data;
    int length
}SqlList;	//顺序表类型
SqlList L;
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);

操作算法中用到的预定义常量和类型

//函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status 是函数的类型,其值是函数结果状态代码
typedef int Status;
typedef char ElemType;

构造线性表

静态:
#define MAXSIZE 100
typedef struct{
    
    
    ElemType elem[MAXSIZE];
    int length;
} SqlList;
动态数组:
typedef struct{
    
    
    ElemType *elem;
    int length;
} SqlList;	//顺序表类型
L.elem=(ElemType*)malloc(sizeof(ElemType)*MAXSIZE);

顺序表的查找

算法思想:
在线性表L中查找与指定值e相同的数据元素的位置
从表的一端开始,逐个进行记录的关键字和给定值的比较。找到,返回该元素的位置序号,未找到,返回0。

int LocateELem(SqlList L, ElemType e){
    
    
    //在线性表L中查找值为e的数据元素,返回其序号(是第几个元素	)
    for(i=0;i<L.length;i++)
        if(L.elem[i]==e) return i+1;//查找成功,返回序号
    return 0;//查找失败,返回0
}

顺序表的插入

算法思想:
1.判断插入位置i是否合法。
2.判断顺序表的存储空间是否已满,若已满返回ERROR。
3.将第n至第i位的元素依次向后移动一个位置,空出第i个位置.
4.将要插入的新元素e放入第i个位置。
5.表长加1,插入成功返回OK。

Status Listinsert_Sq(SqlList &L, int i, ElemType e){
    
    
    if(i<1 || i>L.length+1) return ERROR;	//i值不合法
    if(L.length==MAXSIZE) return ERROR;//当前储存空间已满
    for(j=L.length-1; j>=i-1; j--)
        L.elem[j+1]=L.elem[j];	//插入位置及之后的元素右移
    L.elem[i-1]=e;	//将新元素e放在第i个位置 
    L.length++;		//表长增1
    return OK;
}

顺序表的删除

算法思想:
1.判断删除位置i是否合法(合法值为1si≤n)。
2.将欲删除的元素保留在e中
3.将第i+1至第n位的元素依次向前移动一个位置。
4.表长减1,删除成功返回OK。

Status ListDelete_Sq(SqlList &L, int i){
    
    
    if((i<1)||(i>L.length)) return ERROR;//i值不合法
    for(j=i; j<=L.length-1; j++)
        L.elem[j-1]=L.length[j];//被删除元素后的元素前移
    L.length--;	//表长减1
    return OK;
}

顺序表(线性表的顺序存储结构)的特点

(1)利用数据元素的存储位置表示线性表中相邻数据元素之间的前后大系,
即线性表的逻辑结构与存储结构一致
(2)在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。
因此可以粗略地认为,访问每个元素所花时间相等

这种存取元素的方法被称为随机存取法

顺序表优缺点

优点:
存储密度大(结点本身所占存储量/结点结构所占存储量)
可以随机存取表中任一元素
缺点:
在插入、删除某一元素时,
要移动大量元素
浪费存储空间
属于静态存储形式,数据元素的个数不能自由扩充

3.线性表的链式表示和实现

结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
线性表的链式表示又称为非顺序映像链式映像
用一组物理位置任意的存储单元来存放线性表的数据元素。
这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。
链表中元素的逻辑次序和物理次序不一定相同。

与链式储存有关的术语:

1.结点:数据元素的储存映像。由数据域和指针域两部分组成
2.链表:n个结点由指针链组成一个链表。
它是线性表的链式储存映像,称为线性表的链式储存结构
3.单链表,双链表,循环链表:

  • 结点只有一个指针域的链表,称为单链表或线性链表
  • 结点有两个指针域的链表,称为双链表
  • 首尾相接的链表称为循环链表

4.头指针,头结点和首元结点:
在这里插入图片描述
头指针:是指向链表中第一个结点的指针
首元结点:是指链表中存储第一个数据元素a 1 _1 1的结点
头结点:是在链表的首元结点之前附设的一个结点

单链表由表头唯一决定,因此单链表可以用头指针的名字来命名,若头指针名是L,则把链表称为表L

单链表的存储结构

类型定义:
typedef Struct{
    
    
    char num[8];	//数据域
    char name[8];	//数据域
    int score;		//数据域
}ElemType;

typedef struct Lnode{
    
    	//声明结点的类型和指向结点的指针类型
    ElemType *data;		//结点的数据域
    struct Lnode *next;	//结点的指针域
}Lnode, *LinkList;	//LinkList为指向结构体Lnode的指针类型

变量定义:
定义链表L:LinkList L;//指向头结点,定义头指针
定义结点指针p: LNode *p; 等价于 LinkList p;
类型定义:
p=L; //p指向头结点
s=L->next //s指向首元结点
p=p->next //p指向下一结点

单链表基本操作的实现

  • 单链表的销毁

  • 清空单链表

  • 求单链表表长

  • 判断链表是否为空

  • 取值:取单链表中第i个元素的内容

  • 查找:

    • 按值查找:根据指定数据获取该数据所在的位置(该数据所在的地址)
    • 按值查找:根据指定数据获取该数据所在的位置序号(是第几个数据元素)
  • 插入:在第i个结点前插入新结点

  • 删除:删除第i个结点

  • 单链表的建立

    • 前插法(头插法)
    • 尾插法(尾插法)

单链表的初始化(带头结点的链表)

构造一个空表

算法步骤:
1.生成新结点作头结点,用头指针L指向头结点
2.将头结点的指针域置空

算法描述:

Status InitList_L(LinkList &L){
    
    
    或L=(LinkList)malloc(sizeof(LNode));
    L->next=NULL;
    return OK;
}

判断链表是否为空

算法思路:判断头结点指针域是否为空

int ListEmpty(LinkList L){
    
    
    if(L->next)//非空
        return 0;
    else
        return 1;
}

单链表的销毁(链表销毁后不存在)

算法思路:从头指针开始,依次释放所有结点

Status DestoryList_L(LinkList &L){
    
    
    Lnode *p;	//或LinkList p
    while(L){
    
    
        p=L;
        L=L->next;
        delete p;
    }
    return OK;
}

清空链表

链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)

算法思路:依次释放所有结点,并将头结点指针域置为空

Status ClearList(LinkList &L){
    
    
    Lnode *p,*q;	//或LinkList p,q
    p=L->next;
    while(p){
    
    		//没到表尾
        q=p->next;
        delete p;
        p=q;
    }
    L->next=NULL;//头结点指针域为空
    return OK;
}

求单链表表长

算法思路:从首元结点开始依次计数所有节点

int ListLength_L(LinkList L){
    
    //返回L中数据元素个数
    LinkList p;
    p=L->next;		//p指向第一个结点
    i=0;
    while(p){
    
    		//遍历单链表,统计结点数
        i++;
        p=p->next;
    }
    return i;
}

取值,取单链表中第i个元素的内容

算法思路:分别取出表中第3个元素和第15个元素
从链表的头指针出发,顺着链域next逐个节点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构

算法步骤:
1.从第一个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初值p=L->next
2.j做计数器,累计当前扫描过的结点数,j初值为1
3.当p指向扫描到的下一结点时,计数器j+1
4.当j==i时,p所指的结点就是要找的第i个结点

算法描述:

Status GetElem_L(LinkList L, int i, ElemType &e){
    
    
    //获取线性表L中某个数据元素的内容,通过变量e返回
    p=L->next; j=1;	//初始化
	while(p&&j<i){
    
    	//向后扫描,直到p指向第i个元素或p为空
        p=p->next;
        ++j;
	}
	if(!p||j>i) return ERROR;//第i个元素不存在
	e=p->data;
    return OK;
}//GetElem_L

按值查找–根据指定数据获取该数据所在的位置(地址)

算法步骤:
1.从第一个结点起,依次和e相比较。
2.如果找到一个其值与e相等的数据元素,则返回其在链表中的位置或地址;
3.如果查遍整个链表都没有找到其值和e相等的元素,则返回0或NULL。

算法描述:

Lnode *LocateElem_L(LinkList L, ElemList e){
    
    
    //在线性表L中查找值为e的数据元素
    //找到,则返回L中值为e的数据元素的地址,查找失败返回NULL
    p=L->next;
    while(p&&p->data!=e)
        p=p->next;
    return p;
}

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

插入–在第i个结点前插入值为e的新结点

算法步骤
1.首先找到a i − 1 _{i-1} i1的储存位置p
2.生成一个数据域为e的新结点s
3.插入新结点:1)新结点的指针域指向结点a i _i i
2)结点a i − 1 _{i-1} i1的指针域指向新结点

1.s->next=p->next
2.p->next=s

在这里插入图片描述
算法描述:

Status ListInsert_L(LinkList &L, int i, ElemType e){
    
    
    p=L;j=0;
    while(p && j<i-1){
    
    p=p->next;++j;}//寻找第i-1个结点,p指向i-1结点
    if(!p||j>i-1) return ERROR;//i大于表长+1或者小于1,插入位置非法
    s=new LNode; s->data=e;
    s->next=p->next;//将结点s插入L中
    p->next=s;
    return OK;
}//ListInsert_L

删除–删除第i个结点

算法步骤:
1.首先找到a i − 1 _{i-1} i1的储存位置p,保存要删除的a i _i i的值
2.令p—>next 指向a i + 1 _{i+1} i+1
3.释放结点a i _i i的空间

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

在这里插入图片描述

算法描述:

//将线性表L中第i个数据元素删除
Status ListDelete_L(LinkList &L, int i, ElemType &e){
    
    
    p=L;j=0;
    while(p->next && j<i-1){
    
    p=p->next;++j; }
    	//寻找第i个结点,并令p指向其前驱
    if(!(p->next)||j>i-1)return ERROR;//删除位置不合理
    q=p->next;//临时保存被删结点的地址以备释放
    p->next=q->next;//改变删除结点前驱结点的指针域 
    e=q->data;//保存删除结点的数据域
    delete q;//释放删除节点的空间
    return OK;
}//ListDelete_L

建立单链表:头插法–元素插入在链表头部,也叫前插法

1.从一个空表开始,重复读入数据;
2.生成新结点,将读入数据存放到新结点的数据域中
3.从最后一个结点开始,依次将各结点插入到链表的前端

例如:建立链表L(a, b, c, d, e)

1.在内存中找到一块空间作为头结点并将头结点的指针域置空
L=(LinkList)malloc(sizeof(LNode));
生成空间 数据域放上新元素
LNode p; 	p->data=a;
将新结点插入到所有结点之前  新结点接到头结点后面
p->next=L->next;	L->next=p;

LNode p; 	p->data=a n-1;

p->next=L->next; 	L->next=p;

算法描述:

void CreateList_H(LinkList &L, int n){
    
    
    L=new LNode;
    L->next=NULL;//先建立一个带头结点的单链表
    for(i=n;i>0;--i){
    
    
        p=(LNode*malloc(sizeof(LNode)));//生成新结点
        scanf(p->data);//输入元素值
        p->next=L->next;//插入到表头
        L->next=p;
    }
}//CreateList_H

建立单链表:尾插法–元素插在链表尾部,也叫后插法

1.从一个空表L开始,将新结点逐个插入到链表的尾部(尾指针r指向链表
的尾结点。
2.初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
p->data=a i _i i
p-next=NULL
r->next=p
r=p

算法描述:

//正位序输入n个元素的值,建立带表头结点的单链表L
void CreateList_R(LinkList &L, int n){
    
    
    L=new LNode;	L->next=Null;
    r=L;	//尾指针r指向头结点
    for(i=0;i<n;++i){
    
    
        p=(LNode*malloc(sizeof(LNode)));//生成新结点
        scanf(p->data);//输入元素值
        p->next=NULL;
        r->next=p;	//插入到表尾
        r=p;	//r指向新的尾结点
    }
}//CreateList_R

循环链表

循环链表:是一种头尾相接的链表(即:表中最后一个结点的指计域指向头结点,整个链表形成一个环)。
优点:从表中任一结点出发均可找到表中其他结点

注意:
由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。

带尾指针循环链表的合并

算法描述:

LinkList Connext(LinkList Ta, LinkList Tb){
    
    
    		//假设Ta,Tb都是非空的单循环链表
    p=Ta->next;//p存表头结点
    Ta->next=Tb->next->next;//Tb表头连结Ta表尾
    delete Tb->next;//释放Tb表头结点
    Tb->next=p;//修改指针
    return Tb;
}

双向链表

双向链表的结构可定义如下:

typedef struct DuLNode{
    
    
    Elemtype	data;
    struct DuLNode *prior,*next;//前驱指针与后继指针
}DuLNode,*DuLinkList;

双向循环链表
和单链的循环表类似,双向链表也可以有循环表
让头结点的前驱指针指向链表的最后一个结点
让最后一个结点的后继指针指向头结点。

双向链表的插入

在这里插入图片描述

1.s->prior=p->prior
2.p->prior->next=s
3.s->next=p
4.p->prior=s

void ListInsert_DuL(DuLinkList &L,int i,ElemType e){
    
    
    //在带头结点的双向循环链表L中第i个位置之前插入元素e
    if(!(p=GetElemP_DuL(L,i)))	return ERROR;
    s=new DuLNode;	s->date=e;
    s->prior=p->prior;	p-prior->next=s;
    s-next=p;			p->prior=s;
    return OK;
}//ListInsert_DuL

双向链表的删除

1.p->prior->next=p->next;
2.p->next->prior=p->prior;

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

4.单链表,循环链表和双向链表的 时间效率比较

在这里插入图片描述

5.顺序表和链表的比较

  • 链式存诸结构的优点:

    • 结点空间可以动态申请释放;
    • 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素
  • 链式存储结构的缺点:

    • 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
    • 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。
      在这里插入图片描述

6.线性表的应用

线性表的合并

问题描述:
假设利用两个线性表La和Lb分别表示两个集合A和B,现要求一个新的集合A=AUB
La=(7,5,3,11) Lb=(2,6,3) -----> La=(7,5,3,11,2,6)

算法步骤:
依次取出Lb中的每个元素,执行以下操作:
1.在La中查找该元素
2.如果找不到,则将其插入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(&L,++La_len,e)
    }
}

有序表的合并–用顺序表实现

问题描述:
已知线性表La和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列。
La=(1,7,8) Lb=(2,4,6,8,10,11) -----> Lc=(1,2,4,6,7,8,8,10,11)

算法步骤:
(1)创建一个空表Lc
(2)依次从La或Lb中"摘取"元素值较小的结点插入到Lc表,直至其中一个表变空为止
(3)继续将La或Lb其中一个表的剩余结点插入在Lc表的最后

void MergeList_Sq(SqlList LA,SqlList LB,SqlList &LC){
    
    
    pa=LA.elem;
    pb=LB.elem;
    //指针pa和pb的初值分别指向两个表的第一个元素
    LC.length=LA.length+LB.length;
    //新表长度为待合并两表的长度之和
    LC.elem=new ElemType[LC.length];
    //为合并后的新表分配一个数组空间
    pc=LC.elem;
    //指针pc指向新表的第一个元素
    pa_last=LA.elem+LA.length-1;
    //指针pa_last指向LA表的最后一个元素
    pb_last=LB.elem+LB.length-1;
    //指针pb_last指向LB表的最后一个元素
    while(pa<=pa_last && pb<=pb_last){
    
    
        //两个表都非空
        if(*pa<=*pb)*pc++=*pa++;
        //依次“摘取”两表中值较小的结点
        else *pc++=*pb++;
    }
    while(pa<=pa_last)*pc++=*pa++;
    //LB表已到达表尾,将LA中剩余元素加入LC
    while(pb<=pb_last)*pc++=*pb++;
    //LA表已到达表尾,将LB中剩余元素加入L
}//MergeList Sq

有序表合并–用链表实现

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;			//释放Lb的头结点
    
}

猜你喜欢

转载自blog.csdn.net/qq_53548177/article/details/121862521
今日推荐