数据结构之线性表

参考书籍:《大话数据结构》


线性表说明

 线性表(List):零个或多个数据元素的有限序(元素之间有顺序,第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继)。
 线性表元素的个数n(n≥0)定义为线性表的长度,当n=0时,称为空表。
在这里插入图片描述

线性表的抽象数据类型

ADT   线性表(List)
Data    
      线性表的数据对象集合为{a1,a2,......an},每个元素的类型均为DataType.其中,
除第一个a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,
每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation 
      InitList(*L);         初始化操作,建立一个空的线性表L
      ListEmpty(L);         若线性表为空,返回true,否则返回false
      ClearList(*L);        将线性表清空
      GetElem(L,i,*e);      将线性表L中的第i个位置元素值返回给e
      LocateElem(L,e);      在线性表L中查找与给定值e相等的元素,
如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。
      ListInsert(*L,i,e);   在线性表L中的第i个位置插入新元素e
      ListDelete(*L,i,*e);  删除线性表L中第i个位置元素,并用e返回其值
      ListLength(L);        返回线性表L的元素个数
      endADT 

例,使用上面的函数实现功能:

/*将所有的在线性表Lb中但不在La中的数据元素插入到La中*/
void union(List *La,List *Lb)
{
	int La_len,Lb_len,i;
	ElemType e;			      /*声明与La和Lb相同的数据元素e*/	
	La_len = ListLength(La);  /*求线性表的长度*/
	Lb_len = ListLength(Lb);  
	for(i=1; i<=Lb_len; i++)
	{
		GetElem(Lb,i,e);      /*取Lb中第i个数据元素赋给e*/
		if(!LocateElem(La,e,equal))   /*La中不存在和e相同数据元素*/
		{
			ListInsert(La, ++La_len, e);   /*插入*/
		}
	}
}

线性表的顺序存储结构

 线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
 构建顺序存储结构需要三个属性:

  1. 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置;
  2. 线性表的最大存储容量:数组长度MaxSize;
  3. 线性表的当前长度:length。
/*线性表顺序存储结构代码*/
#define MAXSIZE 20    /*存储空间初始分配量*/
typedef int ElemType; /*数据元素类型*/
typedef struct 
{
	ElemType data[MAXSIZE];   /*数据存储数据元素,最大值为MAXSIZE*/
	int length;               /*线性表当前长度*/
}SqList;

顺序存储结构的插入与删除模式:

优先考虑异常后再进行插入和删除操作

插入步骤

  1. 如果插入位置不合理,抛出异常;
  2. 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
  3. 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
  4. 将要插入元素填入位置i处;
  5. 表长加1。
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(SqList *L, int i, ElemType e)
{
  int k;
  /*顺序线性表已经满,线性表长等于线性表的最大存储容量*/
  if(L->length==MAXSIZE)     
  {
    return ERROR;
  }
  /*当i的位置不在数组下标范围内*/
  if(i<1 || i>L->length+1)    
  {
    return ERROR;
  }
  /*若插入数据位置不在表尾*/
  if(i<=L -> length)          
  {
    /*将要插入位置后数据元素向后移动一位*/
    for(k=L->length-1;k>=i-1;k--)
    {
      L->data[k+1]=L->data[k]);
    }                  
  }
  /*将新元素插入*/
  L->data[i-1] = e;           
  L->length ++;
  return OK;
}

删除步骤

  1. 如果插入位置不合理,抛出异常;
  2. 取出删除元素;
  3. 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
  4. 表长减1
/*初始条件:顺序线性表L已存在,l<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L,int i, ElemType *e)
{
  int k;
  /*线性表为空*/
  if(L->length==0)        
  {
    return ERROR;
  }
  /*删除位置不正确*/
  if(i<1 || i>L->length)  
  {
    return ERROR;
  }
  *e=L->data[i-1];
  if(i<L->length)
  {
    /*将删除位置后继元素前移*/
    for(k=i;k<L->length;k++)  
    L->data[k-1]=L->data[k];
  }
  L->length--;
  return OK;
}    

线性表顺序存储结构的优缺点:
 线性表的顺序存储结构,在存、读数据时,不管哪个位置,时间复杂度都是O(1);而插入或删除时,时间复杂度都是O(n)。说明它比较适合元素个数不太变化,而更多是存取数据的应用。
 优点:

  • 无须为表示表中元素之间的逻辑关系而增加额外的存储空间;
  • 可以快速地存取表中任一位置的元素;

缺点:

  • 插入和删除操作需要移动大量元素;
  • 当线性表长度变化比较大时,难以确定存储空间的容量;
  • 造成存储空间的“碎片”

线性表的链式存储结构

 为了表示每个数据元素ai与其后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域(存储的信息称为指针或链)。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。

	typedef struct Node
	{
	    ElemType data;
	    struct Node *next;
	}Node;
typedef struct Node *LinkList;   /*定义LinkList*/

单链表

单链表的读取:

获得链表第i个数据的算法思路:

  1. 声明一个结点p指向链表第一个结点,初始化 j 从 1 开始;
  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
  3. 若链表末尾p为空,则说明第i个元素不存在;
  4. 否则查找成功,返回结点p的数据

 注:读取思想就是重头开始找,直到第i个元素为止。所以其最坏情况的时间复杂度是O(n)。且由于单链表结构中没有定义表长,故不方便用for来控制循环。其主要核心思想就是“工作指针后移”。

/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L, int i, ElemType *e)
{
    int j;
    LinkList p;     /*声明一结点p*/
    p = L->next;    /*让p指向链表L的第一个结点*/
    j = 1;          /*j为计数器*/
    while(p && j<i) /*p不为空或者计数器j还没有等于i时,循环继续*/
    {
        p = p->next;  /*让p指向下一个结点*/
        ++j;
    }
    if(!p || j>i)
        reruen ERROR;  /*第i个元素不存在*/
    *e = p->data;      /*取第i个元素的数据*/
    return OK;
}    

插入与删除

插入:

s->next = p->next; p->next = s; 让p的后继结点改成s的后继结点,再把结点s变成p的后继结点。
在这里插入图片描述
 单链表第i个数据插入结点的算法思路:

  1. 声明一结点p指向链表第一个结点,初始化 j 从1开始;
  2. 当 j < i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j 累加1;
  3. 若到链表末尾 p 为空,则说明第 i 个元素不存在;
  4. 否则查找成功,在系统中生成一个空结点s;
  5. 将数组元素e赋值给s->data;
  6. 单链表的插入标准语句 s->next = p->next; p->next = s;
  7. 返回成功。
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L),*/
/*操作结果:在L中第i个位置之前输入新的数据元素e,L的长度加1*/
Status ListInsert(LinkList *L, int i, ElemType e)
{
	int j;
	LinkList p,s;
	p = *L;
	j = 1;
	/*寻找第i个结点*/
	while(p && j<i)
	{
		p = p->next;
		++j;
	}
	if(!p || j>i)
	{
		/*第i个元素不存在*/
		return ERROR;    
	}
	/*生成新结点*/
	s = (LinkList)malloc(sizeof(Node));
	s->data = e;
	s->next = p->next;   /*将p的后继结点赋值给s的后继*/
	p->next = s;		 /*将s赋值给p的后继*/
	return OK;
}
删除:

q=p->next; p->next = q->next; 让p的后继的后继结点改为p的后继结点。
在这里插入图片描述
单链表第i个数据删除结点的算法思路:

  1. 声明一结点 p指向链表第一个结点,初始化 j从1开始;
  2. 当 j<i时,就遍历链表,让 p的指针向后移动,不断指向下一个结点, j 累加1;
  3. 若到链表末尾 p为空,则说明第 i 个元素不存在;
  4. 否则查找成功,将欲删除的结点 p->next赋值给q;
  5. 单链表的删除标砖语句 p->next = q->next;
  6. 将q结点中的数据赋值给e,作为返回;
  7. 释放q结点;
  8. 返回成功。
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(LinkList *L, int i, ElemType *e)
{
	int j;
	LinkList p,q;
	p = *L;
	j = 1;
	/*遍历寻找第i个元素*/
	while(p->next && j<i)
	{
		p = p->next;
		++j;
	}
	if(!(p->next) || j>i)
	{
		/*第i个元素不存在*/
		return ERROR;	
	}
	q = p->next;
	p->next = q->next;   /*将q的后继赋值给p的后继*/
	*e = q->data;        /*将q结点中的数据给e*/
	free(q);             /*释放内存空间*/
	return OK;
}

 从单链表的算法中,发现插入或删除的时间复杂度都是O(n)。如果在不知道第 i 个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构时没有太大优势。对于插入或删除数据超频繁的操作,单链表的优势比顺序存储结构就明显很多了,后者查找和插入或删除的时间复杂度都是O(n),而前者的查找时O(n),但插入或删除的时间复杂度是O(1);

单链表的整表创建:

 创建单链表的过程就是一个动态生成链表的过程。 即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。

单个链表创建的算法思路(头插法):
  1. 声明一个结点 p 和计数器变量i;
  2. 初始化一空链表 L;
  3. 让 L 的头结点的指针指向 NULL,即建立一个带头结点的单链表;
  4. 循环;
    a.生成一新结点赋值给 p;
    b.随机生成一数字赋值给p的数据域p->data;
    c.将 p 插入到头结点与前一新结点之间。
    在这里插入图片描述
/*随机产生 n 个元素的值,建立带表头结点的单链线性表 L(头插法)*/
void CreateListHead(LinkList *L, int n)
{
    LinkList p;
    int i;
    srand( time(0));     /*初始化随机数种子*/
    *L = (LinkList)malloc (sizeof(Node));
    (*L) ->next = NULL;     /*先建立一个带头结点的单链表*/
    for( i=0; i<n; i++)
    {
        p = (LinkList)malloc(sizeof(Node));   /*生成新结点*/
        p ->data = rand( ) %100+1;     /*随机生成100以内的数字*/
        p->next = (*L)->next;
        (*L)->next = p;              /*插入到表头*/
    }
}

尾插法示例
 尾插法:创建过程中把新结点放到最后排队插入,先来后到。每次把新结点都插在终端结点的后面。

void CreateListTail ( LinkList *L, int n)
{
        /*L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表*/
        LinkList p,r;
        int i;
        srand( time (0));                                  /*初始化随机数种子*/
        *L = (LinkList) malloc (sizeof (Node));   /*为整个线性表*/
        r = *L;                            /*r 为指向尾部的结点*/
      for( i= 0; i<n; i++)
      {
            p = (Node  *)malloc(sizeof(Node));          /*生成新结点*/
            p->data = rand( )%100 + 1;  /*随机生成100以内的数字*/
            r->next = p;    /*将表尾终端结点的指针指向新结点*/            
            r = p;                   /*将当前的新结点定义为表尾终端结点*/
       }
       r->next = NULL;      /*表示当前链表结束*/
}

单链表的整表删除:

 当不打算使用单链表时,需要把它销毁,就是在内存中把它给释放掉,以便留出空间给其他程序或软件使用。

单链表整表删除的算法思路如下:

  1. 声明一结点p和q;
  2. 将第一个结点赋值给p;
  3. 循环:
    a.将下一个结点赋值给q;
    b.释放p;
    c.将q赋值给p;
      /*初始条件:顺序线性表 L 已存在,操作结果:将 L 重置为空表*/
      Status ClearList( LinkList  *L)
      {
	 LinkList  p,q;
	 p = (*L)->next;      /* p 指向第一个结点*/
	 while(p)                 /*是否到表尾*/
	 {
	      q = p->next;
	      free( p );
	      p = q;
	 }
	(* L) ->next = NULL;    /*头结点指针域为空*/
	 return OK;
       }

循环链表

  在单链表的基础上,将单链表中的尾指针原指向NULL,现在改成指向头指针。两条循环链表合并。
在这里插入图片描述

P = rearA->next;                   /*保存A表的头结点,即①*/
rearA->next = rearB->next->next;   /*将本是指向B表的第一个结点(不是头结                                  点 )赋值给reaA->next即②*/   
rearB->next = p;                   /*将原A表的头结点赋值给rearB->next,即③*/
Free( p );                         /*释放p*/

双向链表

 双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。一个指向直接后继,另一个指向直接前驱。

/*线性表的双向链表存储结构*/
typedef struct DulNode
{
	ElemType data;
	struct DulNode *prior;         /*直接前驱指针*/
	struct DulNode *next;         /*直接后继指针*/
}DulNode, *DuLinkList;

双向链表的插入

 插入顺序:先搞定 s 的前驱和后继,再搞定后结点的前驱,最后解决前结点的后继。
在这里插入图片描述

s -> prior = p;             //1 
s ->next   = p -> next;     //2
p ->next -> prior = s;      //3
p ->next  = s;              //4 

双向链表的删除

在这里插入图片描述

p ->prior->next = p->next;   //1
p ->next->prior = p->prior;  //2
free(p);

静态链表

  用数组描述的链表叫静态链表,也叫游标实现法。该数组的元素都是由两个数据域组成,data 和 cur。data为数据域用来存放数据元素;而游标cur相当于单链表中的 next 指针,存放该元素的后继在数组中的下标。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值