数据结构复习(2.线性表)

1.线性结构特点:

1.只有一个首节点和尾节点

2.除了首位节点之外,其他节点只有一个直接前驱和直接后继

   线性结构反映节点间的逻辑关系是一对一

   线性表的直接定义:用数据元素的有限序列表示


eg:(a1,a2,,,,,,,ai-1,ai,ai+1,,,,,,,an)

a1处:线性起点     ai-1是ai的直接前驱  ai+1是ai的直接后继

           下标是元素的符号,表示元素在表中的位置

            n=0时称为空表,记做L=()    n为线性表的长度

            同一线性表中的元素必定具有相同的个性


1.1判断题

1.数据逻辑结构是指数据元素之间的逻辑关系                         对

2.线性表的逻辑结构定义是唯一的,不依赖于计算机              对·

3.线性表反映节点间的逻辑关系是一对一的                             对

4.一维向量是线性表,但是二维或者n维数组不是                    错

                                                                                                  错


1.2编程题

       假设有两个集合A和B,分别用两个线性表LA和LB表示,即:线性表中的元素即为集合中的成员。现在要求一个新的集合A=A并B

思路:

步骤:

1.取得线性表B中的一个元素:  getElem(LA,i,e) 即将下标为i的元素赋值给e。

   可循坏执行此函数n(LB长度)次。

2.判断此元素在LA中是否存在。locateElem(LA,e,equal());

3.如果e不存在,则插入之。

   listInsert(LA,n+1,e);(n表示线性表LA当前的长度)

可大概得出代码段为:


1.3例三  

 1设置两个指针指向LA和LB中的数据9元素

   getElem(LA,i,ai);

   getElem(LB,i,bi);

2.将其中较小的数据元素插入到LC中,直至一个表为空

   insertElem(LC,++k,e);

   对应的i或者j也要加一

3.将剩下的一个表中的元素以此插入。

   先getElem然后再listInsert(LC,++k,e);


2.2线性表的顺序表示及实现

  2.2.1顺序表的表示

     线性表的顺序表表示又称为顺序存储结构或者顺序映像

     顺序存储的定义:将逻辑上相邻的数据元素存储于物理上相邻的存储单元中

     顺序存储方法:用一组地址相邻的存储存储单元以此存储线性表的元素,可以通过数组V[n]来         实现。

顺序表的特点:

   1.逻辑上相邻的数据元素,其物理上也相邻

   2.若以知表中首元素在存储器中的位置,则其他元素的位置也可以求出     

利用:   

可以求出。 

n计算机内存的存取原理是:如果知道该内存单元的地址,马上就可以读取到里面的内容,速度非常快,称为随机存取

n 顺序表是一种 随机存取 的存储结构 ”,含义为:在顺序表这种存储结构上进行查找操作,其时间性能是 O(1 )
n “链表是一种顺序存取的存储结构”
n 存储结构 存取结构 是两种不同的概念:
ü 存储结构 是数据及其逻辑结构在计算机中的表示。
ü 存取结构 是在某种数据结构上 对查找操作时间性能的描述 。如: 随机存取,顺序存取。        

2.2.2顺序表的实现

# define  LIST_INIT_SIZE   100       //符号常量   // 线性表存储空间的初始分配量
# define   LISTINCREMENT    10     // 线性表存储空间的分配增量
typedef struct {                                    //typedef 给结构类型起别名
       ElemType *elem;                          //表基址
       int                length;                      //表长(特指元素个数)
       int                listsize;                     //表当前存储容量
}SqList

线性表的初始化操作:


Status InitList_Sq(SqList &L){
	//构造一个空的线性表L
	L.elem=(ElemType* )malloc(LIST_INIT_SIZE*sizeof(ElemType)); //为数据元素开辟一维数组空间
	if (!L.elem)  exit(OVERFLOW); //存储分配失败
	L.length=0;   // 空表长度为0
	L.listsize=LIST_INIT_SIZE;   //初始存储容量
	return OK;
}

线性表的销毁操作:

Status DestroyList(SqList &L)
{
	free(L.elem);
	L.elem=NULL;
	L.length=0;
	L.listsize=0;
	return OK;
}

修改  通过数组的下标便可访问某个特定元素并修改之:

status modify(SqList &L, int i, ElemType e){ //注意:i是位置
		
		
        L.elem[i-1]=e; //或者 *(L.elem+i-1)=e	
        return OK;
}

显然,顺序表元素修改操作的时间效率是T(n))=O(1)

在线性表的第i个位置插入一个元素:

   1.判断插入位置i是否是1<=i<=L.length  合法

   2.将第n至第i位的元素向后移一位

   3.把要插入的元素赋值给第i个元素

   4.表长加一

Status listInsert(Sqlist &L,int i,ElementType e){
    ElementType *p;
    p=&L.elem;
    if(i>=1||i<=L.length)
        return ERROR;

    if(L.length>=L.listsize){
        newbase=(ElemType )realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof(ElemType));

    for(int a=L.length-1;a>=i;i--)
        p[a+1]=p[a];
        P[a]=e;
}

      删除顺序表中第i个位置的元

      和上一操作思路大致相同。


以上操作应用举例

已知两个顺序线性表 La Lb 的元素按值非递减排列, 归并 La Lb 得到新的顺序线性表 Lc 也按值非递减排列。

void MergeList_Sq(SqList La,Sqlist Lb,Sqlist &Lc)

void MergeList(List La,List Lb,list &Lc)
   {
       InitList(Lc);
     i=j=1;k=0;
    La_len=Listlength(La); 
    Lb_len=Listlength(Lb);

    while((i<=La_len)&&(j<=Lb_len))
   { 
       GetElem(La,i,ai);GetElem(Lb,j,bj)
       if(ai<=bj) {ListInSert(Lc,++k,ai) ;++i; }
        else{ListInsert(Lc,++k,bj):++j;
}

    while (i<=La_len)
    {GetElem(La,i,ai);
    ListInsert(Lc,++k,ai)}
    }

    while (j<=Lb_len)
    {GetElem(Lb,j,bj);
    ListInsert(Lc,++k,bj)}
    }//MergeList

}

方法二:

Void MergeList_Sq(SqList La, SqList  Lb, SqList  &Lc)
{pa=La.elem; pb=Lb.elem;
Lc.listsize=Lc.length=La.length+Lb.length;
Pc=Lc.elem= =(ElemType *)malloc(Lc.listsize*sizeof(ElemType)); 
If(!Lc.elem)exit(OVERFLOW);
pa_last=La.elem+La.length-1;
pb_last=Lb.elem+Lb.length-1;
While(pa<=pa_last && pb<=pb_last){
if (*pa<=*pb) *pc++=*pa++; 
else *pc++=*pb++; }
While(pa<=pa_last){*pc++=*pa++; }
While(pb<=pb_last){*pc++=*pb++; }      }

2.2.3顺序表的运算效率分析

 插入算法的时间效率分析:

算法时间主要耗费在移动元素的操作上,因此计算时间复杂度的基本操作(最深层语句频度)       T(n)= O ( 移动元素次数 )
移动元素的个数取决于插入或删除元素的位置。
假设在进行插入或者删除操作时对顺序表的每个元素都是等概率的。

p(i)=1/(n+1) ,删除概率q(i)=1/n ,则:

 

因此,时间复杂度T(n)=O(n)

显然,顺序表的空间复杂度S(n)=O(1)

merge算法的时间效率分析: 

        费时在比较算法上面,插入算法由于一直插入至LC末尾,所以时间效率是不变的,根据代              码,总时间效率为T(n)=O(La.length+Lb.length);

3.小结:

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

        逻辑关系上相邻的两个元素在物理存储位置上面也相邻

优点:可以随机的存取表中的任一元素

缺点:在插入删除某一元素时,需要移动大量的元素。


2.3线性表的链式表示和实现

        2.3.1链表的表示

        1.链式存储的特点:

                逻辑上相邻,物理上不一定相邻

示意图

                 每个存储节点都包括两个部分:数据域和指针域(链域)

·                在单链表中,任一节点的存储位置都由该节点的直接前驱的指针域的值

        2.与链式存储有关的术语

                1)结点:数据元素的存储映像。由数据域和指针域两部分组成;
                2)链表: n 个结点由指针链组成一个链表。它是线性表的链式像,                       

                 称为线的        链式存储结构。
                3)单链表、双链表、多链表、循环链表: 
                结点只有一个指针域的链表,称为单链表或线性链表;
                有两个指针域的链表,称为双链表;
                有多个指针域的链表,称为多链表;
                首尾相接的链表称为循环链表。

头指针,头结点,首元结点之间的关系

头指针:指向链表中第一个节点的指针

头结点:在首元结点之前附设的一个结点,数据域内只放空表标志或者表长等信息

首元结点:指链表中存储线性表第一个数据元素a1的节点

例题1:

 

解:

        头指针中存储的是首元结点的地址:即ZHAO的地址31 

注:

        链式存储结构中不一定要有头指针

无头结点时,当头指针的值为空时表示空表;

有头结点时,当头结点的指针域为空时表示空表。

例题二:

        头结点的数据域可以为空吗?

        答:头结点的数据域可以为空,也可存放线性表长度等附加信息,但是此节点不能记入链表长度值。

例题三:

讨论3. 链表的数据元素有两个域,不再是简单数据类型,编程时该如何表示?

        答:因每个结点至少有两个分量,所以要采用结构体数据类型。

Typedef struct Lnode {
     ElemType         data;         //数据域
     struct Lnode   *next;        //指针域
}Lnode, *LinkList;               // LinkList为指向Lnode类型的指针

其中LinkList很重要  因为做辅助指针指向首元结点或者指向插入位置结点时都要用到LinkList

        3.补充:结构数据类型及c语言表示法

      2.3.2链表的实现

        1.单链表的读取(或修改):

        单链表中想取得第i个元素,必须从头指针出发寻找(顺藤摸瓜),不能随机存取 ,只能顺序存取,注意判断i不合法的情况,和表空的情况

        思路:要修改第i个数据元素,关键是要先找到指向该结点的指针p,然后用p->data=new_value 即可。

Status GetElem_L(LinkList L, int i, ElemType &e){
// 注意:L为带头结点的单链表的头指针
// 当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
     P=L->next;      j=1;
                        //j用来计数
     while(p && j<i)  {p=p->next;    ++j;}
     if (!p || j>i)   return ERROR;
//!p即p为空,有两种情况,第一种是表为空;第二种是i的值超过了表的长度,while循环执行完后p走到最后一个元素还没有找到i的位置。j >i 就是i是0或者负数的情况。
     e=p->data;
     return OK;
}

2.单链表的插入:

                1.使LinkList指针指向该插入位置的节点,即第i-1个结点。

                2.元素x结点应预先生成:

                S=(ElemType*)malloc(m);

                S->data=x;

插入时原理示意图 

插入核心语句:s->next=p->next;  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}
		                           //p将指向第i-1个元素
		if(!p || j>i-1) return ERROR;

		s=(LinkList)malloc(sizeof(LNode));
		s->data=e; 

		s->next=p->next;
		p->next=s;

          return OK;
	}

3.单链表的删除操作:

核心语句:

        1.通过while循环使指针p指向第i-1个元素

        2.q=p->next;   

        3.p->next=q->next

        4.free(q);

讨论:在链表中设置头结点有什么好处?

        头结点即在链表的首元结点之前附设的一个结点,该结点的数据域中不存储线性表的数据元素,其作用是为了对链表进行操作时,可以对空表、非空表的情况以及对首元结点进行统一处理,编程更方便。

4.单链表的建立和输出

        实现思路:先开辟头指针,然后陆续为每个数据元素开辟存储空间并赋值,并及时将地址送给前面的指针。

        实现方法:头插法和尾插法。

        头插法:每次插入都插入到最前面。

Void CreateList_L (LinkList &L, int n) {
  //逆位序输入n个元素的值,建立带表头结点的单链线性表L.
   L = (LinkList) malloc (sizeof(LNode));
   L→next = NULL;        // 先建立一个带头结点的单链表
   for ( i =n; i>0; --i) {
        p = (LinkList) malloc (sizeof(LNode)); // 生成新结点
        scanf (&p →data);       // 输入元素值
        p→next = L→next ;  
        L→next = p;              // 插入到表头
    }
}  // CreateList_L

        尾插法:每次插入都插入到尾部。

Void CreateList_L (LinkList &L, int n) {
  //正序输入n个元素的值,建立带表头结点的单链线性表L.
   L = (LinkList) malloc (sizeof(LNode));
   L→next = NULL;        // 先建立一个带头结点的单链表
  q=L;//q总是指向最后一个元素
   for ( i =n; i>0; --i) {
        p = (LinkList) malloc (sizeof(LNode)); //新结点
        scanf (&p →data);        p->next=null; // 输入元素值
        q→next = p;          //把新结点p放到q的后面
        q=q→next ;              
    }
}  // CreateList_L

        最后一段代码很重要:q=q->next;因为q指针必须时刻指向最后一个元素。

例子:尾指针插入法实现26字母链表。

void build(){
   int i;
   head=(LNode*)malloc(m); //head是指向第一个结点     
   p=head;
   for(i=1;i<26;i++) {
	   p->data=i+‘a’-1;         
       q=(LNode *)malloc(m);//q是一个新的结点 
	   p->next=q;
       p=p->next;  //p总是指向最后一个结点
       //最后一个结点的data等待下次的输入              
   }
	p->data=‘z’;  p->next=NULL ; 
}

遍历算法设计:

void travle(LinkList L){
	LinkList p
	cout<<"建立的链表为:";
	for(p=L->next;p!=NULL;p=p->next)
		cout<<p->data<<"  ";
	}

求单链表长度:

void len_LL (LinkList L,int &len){
         len=0;
	LinkList p;
	for(p=L->next;p!=NULL;p=p->next)
	len++;
}

应用:单链表逆置

void (LinkList L){

    LinkList q;
    
    for(q=L->next;q!=NULL;p=p->next)
        listInsert(LinkList &L2,n);//头插法插入元素
}

两个链表的归并(类似于两个线性表归并的思路)

已知:线性表 AB,分别由单链表 LA , LB 存储,其中数据元素按值非递减有序排列,

要求:A B 归并为一个新的线性表C , C 的数据元素仍按值非递减排列  。设线性表 C 单链表 LC 存储。

假设:A=35811),B=268911

预测:合并后 C =2 , 3 ,  5 , 6 , 8 , 8 , 9 , 1111

思路:

 具体为三步:

搜索,比较,插入;

搜索:需要两个LinkList指针指向每一个元素

比较:比较上述指针指向的两个元素的数据域

插入:将数据域小的插入新的链表。        

Void   MergeList_L(LinkList  &La,LinkList &Lb,LinkList &Lc)
  {    //按值排序的单链表LA,LB,归并为LC后也按值排序
    pa=La->next;  pb=Lb->next;   
    Lc=La;pc=La    //用La的头结点,作为Lc的头结点  
    while(pa&&pb)        //将pa 、pb结点按大小依次插入C中
            { 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 ;     //插入剩余段
       free(Lb);                       //释放Lb的头结点
} //MergeList_L

        2.3.3其他形式的链表

讨论:用一位数组可以存放链表吗?

答:能。只要定义一个结构类型(含数据域指示域)数组,就可以完全描述链表,这种链表称为静态链表

注:数据域含义与前面相同,指示域相当于前面的指针域。

静态链表的结构:

#define MAXSIZE  1000

typedef struct{

     ElemType data;

     int cur;

}component,SLinkList[MAXSIZE]

讨论二:链表能不能首位相连:

答:能。只要将表中最后一个结点的指针域指向头结点即可 (P->next=head;) 。这种形成环路的链表称为循环链表

空循环链表指针域指向头结点本身。 

循环链表的操作与线性链表基本一致,差别在于算法中的循环条件不是pp->next是否为空,而是它们是否等于头指针。

双向链表:

空双向链表。

非空双向循环链表。 

双向循环链表的插入和删除:

插入:

Status ListInsert_DuL(DuLinkList &L,int i, ElemType e){
If(!(p=GetElemP_DuL(L,i))) return ERROR; //检查i处是否有结点 并且顺便让p指向i处的结点
If(!(s=(DuLinkList)malloc(sizeof(DulNode)))) return ERROR; //为新节点提前分配空间
s->data=e; //新结点赋值
s->prior=p->prior;  //s前指向第i个结点之前的        
p->prior->next=s;//第i-1个结点指向s结点本身
s->next=p; //s结点指向p                  
p->prior=s;//p结点本身的前指针域指向s
return Ok;
}

删除:

双向链表的删除操作:
Status ListDelete_DuL(DuLinkList &L,int i, ElemType &e){
If(!(p=GetElemP_DuL(L,i))) return ERROR;
e=s->data;
p->prior->next=p->next;
p->next->prior= p->prior;
//两行的顺序是否可以颠倒?  可以。
free(p); return Ok;
}

        2.3.4链表的运算效率的分析

时间效率分析:

1. 查找  因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)

2. 插入和删除    因线性链表不需要移动元素,在给出某个合适位置的指什后,插入和删除操作所需的时间仅为 O(1)

但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n)书中的两个算法的时间复杂度都为O(n)

空间效率分析:

链表中每个结点都要增加一个指针空间,相当于总共增加了n 个整型变量,空间复杂度为 O(n)


3.本章小结

讨论1:线性表的逻辑结构特点是什么

答:只有一个首节点和尾节点,除了首尾节点之外每个节点只有一个直接前驱和直接后继。简言之:线性结构反映的节点之间的裸机价关系是一对一的。

顺序存储时,线性结构中逻辑结构相邻的数据元素物理存放地址也相邻。要求内存中可用存储单元的地址必须是连续的。

线性结构的链式存储中,相邻数据元素可以随意存放,但所占存储空间为两个部分,一部分存储节点值,另一部分存放表示节点之间关系的指针。

讨论2:优缺点:

顺序存储:存储密度大,空间利用率高,但是插入删除元素时不方便

链式存储:优点是插入或者删除元素时不方便,使用灵活,缺点是存储密度小,空间利用率低。讨讨论3:什么情况下用线性表比用链表要好?

顺序表适宜于做查找这样的静态操作;链表宜于做插入、删除这样的动态操作。

若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;

若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值