数据结构-线性表(附代码)

线性结构之线性表

  • 线性结构的基本特点:除第一个元素无直接前驱,最后一个元素无直接后继外,其他每个元素都有一个前驱和后继。
  • 由n个数据特性相同的元素构成的有限序列称为线性表。

线性表的表示和实现

分为顺序存储表示和链式表示,各自都包含:初始化、取值、查找、插入、删除,五个基本操作。

线性表的顺序存储表示

i i i 个元素的存储位置 :
L o c ( a i + 1 ) = L o c ( a i ) + ( i − 1 ) ∗ l Loc(a_{i+1})=Loc(a_i)+(i-1)*l Loc(ai+1)=Loc(ai)+(i1)l
l l l 表示每个元素所占内存大小。

顺序表的存储结构

#define MAXSIZE 100   // 顺序表可能达到的最大长度
typedef struct
{
	ElemType *elem;  //存储空间的基地址
	int length; 	 //当前长度
}SqList; 			 //顺序表的结构
SqList L;            //定义L为SqList变量,便可利用L.elem[i-1]访问表中位置序号为i的数据记录
补充1:C++中的参数传递
  • 函数调用时传送给形参表的实参必须与形参在类型、个数、顺序上保持一致。
  • 参数传递有两种方式:传值方式、传地址(参数可以分别为指针变量、引用类型、数组名)。
    传值方式
    1.把实参的值传送给函数局部工作区相应的副本中,函数使用这个副本执行必要的功能。函数修改的是副本的值,实参的值不变
#include <iostream.h>
void swap(float m,float n)
{float temp;
 temp=m;
 m=n;
 n=temp;
}

void main()
{float a,b;
 cin>>a>>b;
 swap(a,b);
cout<<a<<endl<<b<<endl;
}

2.形参变化影响实参。

#include <iostream.h>
void swap(float *m,float *n)
{float t;
 t=*m;
 *m=*n;
 *n=t;
}

void main()
{float a,b,*p1,*p2;
 cin>>a>>b;
 p1=&a;   p2=&b; 
  swap(p1, p2);
cout<<a<<endl<<b<<endl;
}

3.形参变化不影响实参。

#include <iostream.h>
void swap(float *m,float *n)
{float *t;
 t=m;
 m=n;
 n=t;
}

void main()
{float a,b,*p1,*p2;
 cin>>a>>b;
 p1=&a;   p2=&b; 
  swap(p1, p2);
cout<<a<<endl<<b<<endl;
}

4.引用:给变量起别名。

  • j是一个引用类型, 代表i的一个替代名
    i值改变时,j值也跟着改变,所以会输出
    i=7 j=7
#include<iostream.h>
void main(){
	int i=5;
	int &j=i;
	i=7;
	cout<<"i="<<i<<" j="<<j;
}
  • 利用引用修改形参同时修改实参
#include <iostream.h>
void swap(float& m,float& n)
{float temp;
 temp=m;
 m=n;
 n=temp;
}

void main()
{float a,b;
 cin>>a>>b;
 swap(a,b);
cout<<a<<endl<<b<<endl;
}

5.引用类型作形参的三点说明
(1)传递引用给函数与传递指针的效果是一样的,形参变化实参也发生变化
(2)引用类型作形参,在内存中并没有产生实参的副本,它直接对实参操作;而一般变量作参数,形参与实参就占用不同的存储单元,所以形参变量的值是实参变量的副本。因此,当参数传递的数据量较大时,用引用比用一般变量传递参数的时间和空间效率都好
(3)指针参数虽然也能达到与使用引用的效果,但在被调函数中需要重复使用“*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参
传地址方式
1.数组名做参数:传递的是数组的首地址对形参数组所做的任何改变都将反映到实参数组

用数组作函数的参数,求10个整数的最大数
#include <iostream.h>
#define N 10
int max(int a[]);
void main ( ) {
	int a[10];
	int i,m;
	for(i=0;i<N;i++)
		cin>>a[i];
	m=max(a);
	cout<<"the max number is:"<<m;
}
int max(int b[]){
    int i,n;
    n=b[0];
    for(i=1;i<N;i++)
		if(n<b[i]) n=b[i];
    return n;
}
练习:用数组作为函数的参数,将数组中n个整数按相反的顺序存放,要求输入和输出在主函数中完成
#include <iostream.h>
#define N 10
void sub(int b[ ]){
	int i,j,temp,m;
	m=N/2;
	for(i=0;i<m;i++){
    	j=N-1-i; 
        temp=b[i];
	    b[i]= b[j]; 
        b[j]=temp;	}
	return ;}
	void main ( ) {
	int a[10],i;
	for(i=0;i<N;i++)
		cin>>a[i];
	sub(a);
	for(i=0;i<N;i++)
		cout<<a[i];
}

顺序表的基本操作1——初始化

Status InitList(SqList &L)		  //使用引用是因为函数内需要对L进行修改
{
	L.elem=new ElemType[MAXSIZE]; //为顺序表分配一个大小为MAXSIZE的数组空间
	if(!L.elem) exit(OVERFLOW);	  //存储分配失败则退出
	L.length=0;					  //空表的长度为0
	return OK;
}

也可利用指针
Status InitList_Sq(SqList *L){    //构造一个空的顺序表L
    L-> elem=new ElemType[MAXSIZE];   //为顺序表分配空间
    if(!L-> elem) exit(OVERFLOW);       //存储分配失败
    L->length=0;	            	  //空表长度为0
    return OK;
}
补充2:几个简单操作的基本算法实现
销毁线性表L
void DestroyList(SqList &L)
{
  if (L.elem) delete[]L.elem;    //释放存储空间
}

清空线性表L
void ClearList(SqList &L) 
{
   L.length=0;                //将线性表的长度置为0
}
求线性表L的长度
int GetLength(SqList L)
{
   return (L.length);             
}
判断线性表L是否为空
int IsEmpty(SqList L)
{
  if (L.length==0) return 1;      
   else return 0;
}

顺序表的基本操作2——取值

获取第i个位置的数据

int GetElem(SqList L,int i,ElemType &e)
{
  if (i<1||i>L.length) return ERROR;   
   //判断i值是否合理,若不合理,返回ERROR
  e=L.elem[i-1];   //第i-1的单元存储着第i个数据
  return OK;
}

顺序表的基本操作3——查找

在线性表L中查找值为e的数据元素
int LocateELem(SqList L,ElemType e)
{
  for (i=0;i< L.length;i++)
      if (L.elem[i]==e) return i+1;                
  return 0;
}

时间复杂度分析(这里一定要把握核心思路,切莫死记硬背):
查找第 i i i个元素需要比较 i i i次,每个元素被查找的概率是 1 n \frac{1}{n} n1,则平均查找长度 A S L = ∑ i = 1 n p i C i = 1 n ∑ i = 1 n i = n + 1 2 ASL=\sum_{i=1}^{n}p_iC_i=\frac{1}{n}\sum_{i=1}^{n}i=\frac{n+1}{2} ASL=i=1npiCi=n1i=1ni=2n+1, 所以顺序查找的平均复杂度是 O ( n ) O(n) O(n)

顺序表的基本操作4——插入

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

在线性表L中第i个数据元素之前插入数据元素e,在第i个位置插入需要移动n-i+1个元素
Status ListInsert_Sq(SqList &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;
}

平均复杂度是 O ( n ) O(n) O(n)

顺序表的基本操作5——删除

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

将线性表L中第i个数据元素删除
Status ListDelete_Sq(SqList &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.elem[j];       //被删除元素之后的元素前移  
   --L.length;               	      //表长减1
   return OK;
}

平均复杂度是 O ( n ) O(n) O(n)

顺序表的空间复杂度S(n)=O(1)(没有占用辅助空间)

顺序表的特点总结:随机存取

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

  • 存储密度大(结点本身所占存储量/结点结构所占存储量)
  • 可以随机存取表中任一元素

缺点:

  • 插入、删除都需要移动大量元素
  • 浪费存储空间
  • 属于静态存储,数据元素的个数不能自由扩充

为了克服它的一些缺点,引入链表!

线性表的链式表示和实现

链式存储结构

1.结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻

2.各结点由两个域组成:
数据域:存储元素数值数据
指针域:存储直接后继结点的存储位置

3.单链表、双链表、循环链表:

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

4.头指针、头结点和首元结点

  • 头指针是指向链表中第一个结点的指针
  • 首元结点是指链表中存储第一个数据元素a1的结点
  • 头结点是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息

5.在链表中设置头结点有什么好处?
⒈便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理;
⒉便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。

6.链表(链式存储结构)的特点:顺序存取
优缺点正好和顺序表反者来,可见两者各有千秋,结合具体情况进行数据结构的选择。是查找、删除等操作比较多?数据量已知且规模很大?

单链表的定义

typedef struct LNode{
     ElemType   data;       //数据域
     struct LNode  *next;   //指针域
}LNode,*LinkList;           // *LinkList为Lnode类型的指针
LNode *p = LinkList p

指针变量p:表示结点地址
结点变量*p:表示一个结点
若p->data=ai, 则p->next->data=ai+1

单链表的基本操作1——初始化

Status InitList_L(LinkList &L){ 
   L=new LNode;          // 头指针指向新节点          	
   L->next=NULL;     // 头结点的指针域为空
   return OK; 
} 
补充3:几个简单基本操作的算法实现
销毁
Status DestroyList_L(LinkList &L){
    LinkList p;
       while(L)
        {
            p=L;  
            L=L->next;
            delete p;  
        }
     return OK;
 }
清空
Status ClearList(LinkList & L){
  // 将L重置为空表 
   LinkList p,q;
   p=L->next;   //p指向第一个结点
   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;                             
 }
判断表是否为空
int ListEmpty(LinkList L){ 
//若L为空表,则返回1,否则返回0 
   if(L->next)   //非空 
       return 0;
   else
       return 1;
 }

单链表的基本操作2——取值

//获取线性表L中的某个数据元素的内容
Status GetElem_L(LinkList L,int i,ElemType &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; //取第i个元素 
     return OK; 
}//GetElem_L 

单链表的基本操作3——查找

//在线性表L中查找值为e的数据元素
LNode *LocateELem_L (LinkList L,Elemtype 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;
} 

单链表的基本操作4——插入

在这里插入图片描述

将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间
//在L中第i个元素之前插入数据元素e 
Status ListInsert_L(LinkList &L,int i,ElemType e){ 
     p=L;j=0; 
      while(p&&j<i−1){p=p->next;++j;}	//寻找第i−1个结点 
      if(!p||j>i−1)return ERROR;	//i大于表长 + 1或者小于1  
      s=new LNode;			//生成新结点s 
      s->data=e;      		           //将结点s的数据域置为e 
      s->next=p->next;	   	          //将结点s插入L中 
      p->next=s; 
      return OK; 
}//ListInsert_L 

单链表的基本操作5——删除

在这里插入图片描述

//将线性表L中第i个数据元素删除
 Status ListDelete_L(LinkList &L,int i,ElemType &e){
    p=L;j=0; 
    while(p->next &&j<i-1){//寻找第i个结点,并令p指向其前驱 
        p=p->next; ++j; 
    } 
    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.查找只能挨个访问,时间复杂度O(n)
2.删除、插入仅需修改指针,时间复杂度O(1),但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n) 。

单链表的基本操作6——前插法

在这里插入图片描述

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

单链表的基本操作7——尾插法

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

循环链表的定义

在这里插入图片描述
在这里插入图片描述

循环链表的基本操作1——合并

在这里插入图片描述

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

双向链表的定义

在这里插入图片描述

双向链表的基本操作1——插入

在这里插入图片描述

Status ListInsert_DuL(DuLinkList &L,int i,ElemType e){
   if(!(p=GetElemP_DuL(L,i))) return ERROR;
   s=new DuLNode; 
   s->data=e;
   s->prior=p->prior;  
   p->prior->next=s;
   s->next=p;  
   p->prior=s;
   return OK;
}

双向链表的基本操作2——删除

在这里插入图片描述

Status ListDelete_DuL(DuLinkList &L,int i,ElemType &e){
   if(!(p=GetElemP_DuL(L,i)))     return ERROR;
   e=p->data;
   p->prior->next=p->next;
   p->next->prior=p->prior;
   delete p; 
   return OK;
}

总结

在这里插入图片描述

习题

(1)将两个递增的有序链表合并为一个递增的有序链表。要求结果链表仍使用原来两个链表的存储空间, 不另外占用其它的存储空间。表中不允许有重复的数据。
[题目分析]
合并后的新表使用头指针Lc指向,pa和pb分别是链表La和Lb的工作指针,初始化为相应链表的第一个结点,从第一个结点开始进行比较,当两个链表La和Lb均为到达表尾结点时,依次摘取其中较小者重新链接在Lc表的最后。如果两个表中的元素相等,只摘取La表中的元素,删除Lb表中的元素,这样确保合并后表中无重复的元素。当一个表到达表尾结点,为空时,将非空表的剩余元素直接链接在Lc表的最后。
[算法描述]

void MergeList(LinkList &La,LinkList &Lb,LinkList &Lc)
{//合并链表La和Lb,合并后的新表使用头指针Lc指向
  pa=La->next;  pb=Lb->next;    
   //pa和pb分别是链表La和Lb的工作指针,初始化为相应链表的第一个结点
   Lc=pc=La;  //用La的头结点作为Lc的头结点
   while(pa && pb)
{if(pa->data<pb->data){pc->next=pa;pc=pa;pa=pa->next;}
     //取较小者La中的元素,将pa链接在pc的后面,pa指针后移
     else if(pa->data>pb->data) {pc->next=pb; pc=pb; pb=pb->next;}
      //取较小者Lb中的元素,将pb链接在pc的后面,pb指针后移
     else //相等时取La中的元素,删除Lb中的元素
{pc->next=pa;pc=pa;pa=pa->next;
      q=pb->next;delete pb ;pb =q;
}
     }
 pc->next=pa?pa:pb;    //插入剩余段
     delete Lb;            //释放Lb的头结点
}  

(2)将两个非递减的有序链表合并为一个非递增的有序链表。要求结果链表仍使用原来两个链表的存储空间, 不另外占用其它的存储空间。表中允许有重复的数据。
[题目分析]
合并后的新表使用头指针Lc指向,pa和pb分别是链表La和Lb的工作指针,初始化为相应链表的第一个结点,从第一个结点开始进行比较,当两个链表La和Lb均为到达表尾结点时,依次摘取其中较小者重新链接在Lc表的表头结点之后,如果两个表中的元素相等,只摘取La表中的元素,保留Lb表中的元素。当一个表到达表尾结点,为空时,将非空表的剩余元素依次摘取,链接在Lc表的表头结点之后。
[算法描述]

void MergeList(LinkList& La, LinkList& Lb, LinkList& Lc, ) 
{//合并链表La和Lb,合并后的新表使用头指针Lc指向
  pa=La->next;  pb=Lb->next; 
//pa和pb分别是链表La和Lb的工作指针,初始化为相应链表的第一个结点
  Lc=pc=La; //用La的头结点作为Lc的头结点 
  Lc->next=NULL;
  while(pa||pb )
{//只要存在一个非空表,用q指向待摘取的元素
    if(!pa)  {q=pb;  pb=pb->next;}
//La表为空,用q指向pb,pb指针后移
    else if(!pb)  {q=pa;  pa=pa->next;} 
//Lb表为空,用q指向pa,pa指针后移
    else if(pa->data<=pb->data)  {q=pa;  pa=pa->next;}
//取较小者(包括相等)La中的元素,用q指向pa,pa指针后移
    else {q=pb;  pb=pb->next;}
//取较小者Lb中的元素,用q指向pb,pb指针后移
     q->next = Lc->next;  Lc->next = q;   
//将q指向的结点插在Lc 表的表头结点之后
    }
    delete Lb;             //释放Lb的头结点
}   

(3)设计一个算法,通过遍历一趟,将链表中所有结点的链接方向逆转,仍利用原表的存储空间。
[题目分析]
从首元结点开始,逐个地把链表L的当前结点p插入新的链表头部。
[算法描述]

void  inverse(LinkList &L) 
{// 逆置带头结点的单链表 L
    p=L->next;  L->next=NULL;
    while ( p) {
        q=p->next;    // q指向*p的后继
        p->next=L->next;
        L->next=p;       // *p插入在头结点之后
        p = q;
    }
}

(4)删除链表的倒数第 N 个节点LC19
哨兵+快慢指针

/**
一次遍历: 需要使用快慢针 来做定位;
	当快针到达结果的时候;
	慢针正好在 剔除 节点的前节点
注意: 使用哨兵节点防止出现删除首节点的问题
*/
class Solution {
public:
     ListNode* removeNthFromEnd(ListNode* head, int n) {
     // 增加虚拟头节点:哨兵节点; 如果不用; 当删除的节点为 head 节点时候,就会出现异常
	 // 总结: 凡是删除节点的操作,为了保持操作的一致性,都需要引入哨兵节点
     ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;

        ListNode* p = dummyHead;
        ListNode* q = dummyHead;
        for( int i = 0 ; i < n + 1 ; i ++ ){
            q = q->next;
        }

        while(q){
            p = p->next;
            q = q->next;
        }

        ListNode* delNode = p->next;
        p->next = delNode->next;
        delete delNode;

        ListNode* retNode = dummyHead->next;
        delete dummyHead;

        return retNode;
        
    }
};

作者:MisterBooo
链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/dong-hua-tu-jie-leetcode-di-19-hao-wen-ti-shan-chu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

(5)链表的中间节点LC876
利用快慢指针,注意循环条件。

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while(fast != NULL && fast->next!= NULL){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};

(6)环形链表LC141

### 解题思路
解题思路
先假设快的人速度为Vf,慢的人为Vs,跑道长度为n,时间为t,两者的路程即为Sf=Vf*t,Ss=Vs*t
1.先从简单的模型开始思考假设是一个环形跑道,从同一起点出发的两人,一快一慢,那么快的一定会追上慢的,追及时,两者的路程差为跑道的长度,即n=Sf-Ss。
2.此处是离散情况,稍微有些不同,根据等式n=Sf-Ss=t(Vf-Vs),可见两者的速度差必须是n的因子,所以想加快算法的时候要注意不是随便增大两者的速度差就可以的。

其实和上题差不多,新加入了判断语句。
### 代码

```cpp
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast!=NULL && fast->next!= NULL){
            fast = fast->next->next;
            slow = slow->next;
            if(fast==slow){
                return true;
            }
        }
        return false;
    }
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值