数据结构基础----链表题型

数据结构

根据大话数据结构进行总结

数据结构绪论

一些基本定义与概念

  • 计算机解决问题,先从具体问题中抽象出一个适当的数据模型,设计出一个解决此数据模型的算法,然后编写程序,得到一个实际的软件
    -数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系 和操作等相关问题的学科
  • 程序设计=数据结构=算法
  • 数据:是描述客观事物符号,是计算机中可以操作的对象,是指能被计算机识别,比昂输入给计算机处理的符号集合
    对于整型,实型等数值类型,可以进行数值计算。对于字符型数据类型,进行非数值的处理。声音,图像,视频等通过编码的手段变成字符数据来处理
  • 数据元素: 是组成数据的,有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。 如人类中数据元素为人
  • 数据项:一个数据元素可以由若干个数据项组成 如人的嘴巴,鼻子,姓名,年龄。性别等
  • 数据对象: 是性质相同的数据元素的集合,是数据的子集。如人都有姓名,生日,性别等相同的数据项
  • 数据结构: 不同数据元素之间不是独立的,而是存在特定的关系,我们将这些关系成为结构。 所以,数据结构,就是相互之间存在一种或多种特定关系的数据元素的集合。
  • 为编写好一个好的程序,必须分析待处理对象的特性及各处理对象之间存在的关系。这就是研究数据结构的意义所在

逻辑结构 与物理结构

逻辑结构
  • 逻辑结构: 是指数据对象中数据元素之间的相互关系
    1.集合结构: 集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系,各个元素之间是平等的
  1. 线性结构:数据元素是一对一的关系
  2. 树形结构: 一对多
    4.图形结构:多对多
  • 以上,逻辑结构是针对具体问题的,是为了解决某个问题,在对问题的理解的基础上,选择一个合适的数据结构表示数据元素之间的逻辑关系
物理结构
  • 物理结构:是指数据的逻辑结构在计算机中的存储形式
  • 数据的存储结构应正确反映数据元素之间的逻辑关系,关键点
  1. 顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据之间的逻辑关系和物理关系是一致的。
  2. 链式存储结构 :把数举元素存储在任意的存储单元中,这存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个 指针存放数据元素的地址,这样通过地址就可以相关联数据元素的位置。如电影无间道之间的关系

总之,逻辑结构是面向问题的,而物理结构是面向计算机的,其基本的目标就是将数据结构及其逻辑关系存储到计算机的内存中。

数据类型

  • 数据类型:指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
  • 类型用来说明变量或表达式的取值范围和所能进行的操作
  • c语言中,按取值的不同,可以分为两类
原子类型 不可再分,包括整型,实型,字符型
结构类型 由若干个类型组合而成,可以再分解 如整型数组是由若干个整型数据构成的
  • 抽象是指取出事物具有的普遍性的本质。它是抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括。抽象是一种思考问题的方式,它隐藏了繁杂的细节,只保留实现目标的所需的信息

抽象数据类型 一个数学模型及定义在该模型的一组操作,体现了程序设计中问题分解,抽象和信息隐藏的特性

数据结构是相互之间存在的一种或多种特定关系的数据元素的集合

算法

  • 算法:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作
  • 由两种计算1到100累加的算法实现,进行比较,引人深思
  • 为了解决某个或某类问题,需要把指令表示成一定的操作序列,操作序列包括一组操作,每个操作都完成特定的功能,这就是算法了

算法的特性

  • 输入输出:零个或多个输入,一个或多个输出
  • 有穷性:自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成
  • 确定性 :每一步骤都有确定的含义,不会出现二义性
  • 可行性:每一步都呢能够通过执行有限次数完成

算法设计的要求

  • 正确性 :无语法错误,对合法输入数据能够产生满足要求的输出结果,对非法的输入数据能够都到满足规格说明的结果
  • 可读性 便于阅读,理解,交流
  • 健壮性 当输入的数据不合法时,算法也能做出相关处理,而不是产生异常或莫名奇妙的结果
  • 时间效率高和存储量低

算法效率的度量方法

  • 事后统计方法;这种方法主要通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定效率的高低
    有种种缺陷,我们不考虑
  • 事前分析估算法 :在计算机程序编制前,依据统计方法对算法进行估算
    1.算法采用的策略,方法
  1. 编译产生的代码质量
  2. 问题的输入规模
    4, 机器执行指令的速度

算法时间复杂度

  • 大O记法
  • 常数阶 O(1),与n无关
  • 线性阶 O(n), 一般与循环有关
  • 对数阶 O(logn,) while可能
int count = 1;
while count < n)
{
	count = count * 2;
	/* 时间复杂度为O(1)的程序步骤序列 */
}

即2的x次=n,反解出x为logn相关

  • 平方阶 循环嵌套 O(n^2)
  • 常用的时间复杂度所耗费的时间从小到大依次是
    常数阶 log阶 线性阶 n倍线性阶 平方阶 立方阶 2^n阶 n!阶 n^n阶

最坏情况与平均情况

  • 例子 就是说 假如在一个数组中,找一个元素,最好的时间复杂度时O(1),最坏就是O(n),即在最后一个数组元素中找到,我们期望的,或是从概率的角度看,从中间找到这一情况就是平均情况,但我们一般复杂度的计算是指最坏情况

算法空间复杂度

算法很重要

线性表

  • 线性表:零个或多个数据元素的有限序列
  • 概念: 直接前驱元素,直接后继元素,位序
  • 在较复杂的线性表中,一个元素可以由若干个数据组成

线性表的抽象数据类型

ADT 线性表 (List)
Data
     线性表的数据对象集合为 {a1,a2 (⋯⋯,an },每个元素的类型均为 DataTYPe。其中,除第一个元素a₁外,每一个元素有且只有一个直接前驱元素,除了最后一个元素aₙ外,每一个元素 aₙ有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
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的元素个数。  
#include "stdio.h"    

#include "stdlib.h"  
#include "math.h"  
#include "time.h"

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

#define MAXSIZE 20          /* 存储空间初始分配量 */
typedef int ElemType;       /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct
{
  ElemType data[MAXSIZE]; /* 数组,存储数据元素 */
  int length;             /* 线性表当前长度 */
}SqList;

typedef int Status;         /* Status是函数的类型,其值是函数结果状态代码,如OK等 */


Status visit(ElemType c)
{
  printf("%d ",c);
  return OK;
}

/* 初始化顺序线性表 */
Status InitList(SqList *L) 
{ 
  L->length=0;
  return OK;
}

/* 初始条件:顺序线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */
Status ListEmpty(SqList L)
{ 
  if(L.length==0)
  	return TRUE;
  else
  	return FALSE;
}

/* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(SqList *L)
{ 
  L->length=0;
  return OK;
}

/* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(SqList L)
{
  return L.length;
}
  • 细细品味最基础的操作,记牢靠了
  • 以上这些操作时最基本的,更复杂操作可以用基本操做来实现

线性表的顺序存储结构

  • 线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
  • 通俗说一下,有一组类型形同的数据元素要占点地方
    线性表的顺序存储的结构代码
#define MAXSIZE 20          /* 存储空间初始分配量 */
typedef int ElemType;       /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct
{
    ElemType data[MAXSIZE]; /* 数组,存储数据元素 */
    int length;             /* 线性表当前长度 */
}SqList;
  • 三个属性
  1. 存储空间的起始位置,线性表的最大存储量,线性表的当前长度
数组长度与线性表长度的区别

数组长度应大于等于线性表的长度,数组长度一般来说是不变的,线性表随插入·或·删除等操作实时改变

地址计算方法

存取时间性能为O(1),有这一特点的存储结构成为随机存取结构

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

获得元素操作
/* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值,注意i是指位置,第1个位置的数组是从0开始 */
Status GetElem(SqList L,int i,ElemType *e)
{
    if(L.length==0 || i<1 || i>L.length)
            return ERROR;
    *e=L.data[i-1];

    return OK;
}

插入操作
/* 初始条件:顺序线性表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;
	if (i<1 || i>L->length+1)/* 当i比第一位置小或者比最后一位置后一位置还要大时 */
		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;// 可以结合书上的算法思路来理解
}
删除操作
/* 初始条件:顺序线性表L已存在,1≤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),即适合元素个数不太变化,而更多是存取数据的应用

线性表顺序存储结构的优缺点

  • 优点 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
    可以快速地存取表中任一位置的元素
  • 缺点 插入和删除需要移动大量元素
    当线性表移动长度变化较大时,难以确定存储空间的容量
    造成存储空间的“碎片”

线性表的链式存储结构

  • 基础: 一个结点,分为数据域和指针域两部分组成,数据域里存储着数据信息,指针域里存储着指针或称作为链
  • 链表的第一个结点的存储位置叫做头指针,最后一个结点的指针指向空,即null
  • 在单链表的第一个结点前附设一个结点,称为头结点
  • 头指针与头结点的形象描述看53页图片,好看的
  • 结点由存放数据元素的数据域和存放后继结点地址的指针域组成
  • p->data为一个数据元素,p->next为一个指针,即可以为a(i)的指针域,即a(i)的指针域指向a(i+1),即a->next->data为a(i+1)的数据域!
typedef struct Node
{
    ElemType data;
    struct Node *next;
}Node;
typedef struct Node *LinkList; /* 定义LinkList */

单链表的读取


/* 初始条件:链式线性表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 ) 
  	return ERROR;  /*  第i个元素不存在 */
  *e = p->data;   /*  取第i个元素的数据 */
  return OK;
}
  • 由以上可以看出,查找比较麻烦,且不知道具体是那个,所以我们用while循环来便利

单链表的插入和删除

  • 插入 经典语句

s->next = p->next;      /* 将p的后继结点赋值给s的后继  */
	p->next = s;          /* 将s赋值给p的后继 */
  • 这个顺序很重要,新手必要搞反了

/* 初始条件:链式线性表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;
	while (p && j < i)     /* 寻找第i个结点 */
	{
		p = p->next;
		++j;
	} 
	if (!p || j > i) 
		return ERROR;   /* 第i个元素不存在 */
	s = (LinkList)malloc(sizeof(Node));  /*  生成新结点(C语言标准函数) */
	s->data = e;  
	s->next = p->next;      /* 将p的后继结点赋值给s的后继  */
	p->next = s;          /* 将s赋值给p的后继 */
	return OK;
}
  • 删除 经典语句
q = p->next;
	p->next = q->next;			/* 将q的后继赋值给p的后继 */
/* 初始条件:链式线性表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;
	while (p->next && j < i)	/* 遍历寻找第i个元素 */
	{
        p = p->next;
        ++j;
	}
	if (!(p->next) || j > i) 
	    return ERROR;           /* 第i个元素不存在 */
	q = p->next;
	p->next = q->next;			/* 将q的后继赋值给p的后继 */
	*e = q->data;               /* 将q结点中的数据给e */
	free(q);                    /* 让系统回收此结点,释放内存 */
	return OK;
}
  • 综上可以看出,当进行删除或者是插入时,只有一开是复杂度是o(n),而如果插入10个加入,后续的复杂度都是o(1)。

单链表的整表创建

头插法


/*  随机产生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;						/*  插入到表头 */
	}
}

尾插法

/*  随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList *L, int n) 
{
	LinkList p,r;
	int i;
	srand(time(0));                      /* 初始化随机数种子 */
	*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
	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;                       /* 表示当前链表结束 */
}

单链表的整表删除

/* 初始条件:链式线性表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;
}

单链表结构与顺序结构存储结构的优缺点

  • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,做好用单链表结构

静态链表

  • 一些早期的高级编程语言没有指针,他们便想到用数组来来代替指针描述链表,即数组中一个存打他,一个存next,存next的下标我们称为cur,即游标。
/* 线性表的静态链表存储结构 */
typedef struct 
{
    ElemType data;
    int cur;  /* 游标(Cursor) ,为0时表示无指向 */
} Component,StaticLinkList[MAXSIZE];
  • 初始化一个

/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space) 
{
	int i;
	for (i=0; i<MAXSIZE-1; i++)  
		space[i].cur = i+1;
	space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
	return OK;
}
  • 具体的请看书63页图

静态链表的插入操作

  • 需要解决的问题,如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放
  • 每当进行插入时,可以从备用链表上取得第一个结点作为待插入的新节点,一下是代码实现
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space) 
{ 
	int i = space[0].cur;           		/* 当前数组第一个元素的cur存的值 */
	                                		/* 就是要返回的第一个备用空闲的下标 */
	if (space[0]. cur)         
	    space[0]. cur = space[i].cur;       /* 由于要拿出一个分量来使用了, */
	                                        /* 所以我们就得把它的下一个 */
	                                        /* 分量用来做备用 */
	return i;
}


/*  在L中第i个元素之前插入新的数据元素e   */
Status ListInsert(StaticLinkList L, int i, ElemType e)   
{  
    int j, k, l;   
    k = MAXSIZE - 1;   /* 注意k首先是最后一个元素的下标 */
    if (i < 1 || i > ListLength(L) + 1)   
        return ERROR;   
    j = Malloc_SSL(L);   /* 获得空闲分量的下标 */
    if (j)   
    {   
		L[j].data = e;   /* 将数据赋值给此分量的data */
		for(l = 1; l <= i - 1; l++)   /* 找到第i个元素之前的位置 */
		   k = L[k].cur;           
		L[j].cur = L[k].cur;    /* 把第i个元素之前的cur赋值给新元素的cur */
		L[k].cur = j;           /* 把新元素的下标赋值给第i个元素之前元素的ur */
		return OK;   
    }   
    return ERROR;   
}

静态链表的删除操作

/*  删除在L中第i个数据元素   */
Status ListDelete(StaticLinkList L, int i)   
{ 
    int j, k;   
    if (i < 1 || i > ListLength(L))   
        return ERROR;   
    k = MAXSIZE - 1;   
    for (j = 1; j <= i - 1; j++)   
        k = L[k].cur;   
    j = L[k].cur;   
    L[k].cur = L[j].cur;   
    Free_SSL(L, j);   
    return OK;   
} 

静态链表的删除

  • 不一定用得上,但思考方式很巧妙,应理解其思想,以备不时只需。

循环链表

  • 解决了一个问题,如何从当中一个结点出发,访问到链表的全部结点
  • 把两个循环链表合并
p=rearA->next;   			    /* 保存A表的头结点,即① */
rearA->next=rearB->next->next;	/* 将本是指向B表的第一个结点(不是头结点)*/
                 				/* 赋值给reaA->next,即② */
q=rearB->next;
rearB->next=p;				   	/* 将原A表的头结点赋值给rearB->next,即③ */
free(q);					   	/* 释放q */

双向链表

  • 空间换时间,有良好的对称性,使得把某个结点的前后结点的操作带来了方便,可以有效提高算法的时间性能。
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
		ElemType data;
		struct DuLNode *prior;    	/*直接前驱指针*/
		struct DuLNode *next;		/*直接后继指针*/
} DulNode, *DuLinkList;


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

s - >prior = p;   			/*把p赋值给s的前驱,如图中①*/
s -> next = p -> next;		/*把p->next赋值给s的后继,如图中②*/
p -> next -> prior = s;		/*把s赋值给p->next的前驱,如图中③*/
p -> next = s;				/*把s赋值给p的后继,如图中④*/


p->prior->next=p->next;   	/*把p->next赋值给p->prior的后继,如图中①*/
p->next->prior=p->prior;	/*把p->prior赋值给p->next的前驱,如图中②*/
free(p);					/*释放结点*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值