本文由个人笔记整理得出,材料主要来自《大话数据结构》和网络各博主
一、数据结构与算法的关系:
数据结构:一门研究非数值计算的程序问题中的操作对象,以及它们之间关系和操作等相关关系的学科。
说白了就是在一大堆数据中,数据元素之间的关系。(一个问题中多个对象之间的关系)
算法:解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且,每条指令表示一个或多个操作。
说白了就是问题的具体解决方案呗。(特定问题的解决办法)
所以,现在你明白了为什么要将数据结构和算法放在一起谈了对吧?
这两者是互相存在并且互为对方服务的,不放在一起谈根本都谈不了。
二、两者的一些概念:
数据结构的概念框架如下:
数据//由数据对象构成
|
数据对象//数据的子集
|
数据元素//组成数据的基本单位
|
数据项1 数据项2//不可分割的最小单位
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
根据视点的不同,我们把数据结构分为逻辑结构和物理结构:
逻辑结构:集合结构、线性结构、树形结构、图形结构;
物理结构:顺序存储结构、链接存储结构。
算法也有一些概念,具体的就不说了,这里主要说一下算法的空间复杂度S(n)和时间复杂度T(n)。
时间复杂度T(n):在进行算法呢的时候,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。记作:T(n) = O(f(n)),表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作为算法的渐近时间复杂度,简称时间复杂度。其中T(n)是问题规模n的某个函数,我们用O()作为表现时间复杂度的记法。
直接上图:
我们如何来理解这个O(n)函数呢?简单来讲,假如你在整一个程序中,一条语句在只执行一次,此时的时间复杂度为O(1);
如:int listen = 100;//O(1)
listen= listen+listen;//O(2)
listen = listen+listen;//O(3)
printf(“listen:%d”,listen);//O(4)
但是,从更宏观的程序执行流程视野来看,O(4)和O(1)是一样的。为什么?因为在数学里面,O(1)和O(4)的阶项都是一样的,都等于1。所以整个算法的时间复杂度为O(1)。
再举例子如:for(i= 0;i<30;i++),那么此时的时间复杂度为O(30) = O(1);
假如用到的是一个循环语句:for(i= 0;i<n;i++),那么此时的时间复杂度为O(n);
为什么?我们可以从数学的角度上来看,n代表着无穷大,而4,30,100等等这些都是属于同一个阶项的,都等于1;无论你等于多少,只要是等于一个常数,那么就是O(1)。
假如用的是两个for嵌套在一起,那么时间复杂度为O(n的平方);假如这两个for不是嵌套在一起,而是分开的一次执行的,那么时间复杂度还是O(n)。毕竟你两个O(n)在数量级上跟一个O(n)是一样子的嘛,这个我相信对大家不难理解。以上的表格大家一次类推就好。
空间复杂度S(n):计算算法所需要的存储空间,公式S(n) = O(f(n))//n为问题的规模,f(n)为关于n所占存储空间的函数。
举个例子说,要判断某年是不是闰年,你可能会花一点心思来写一个算法,每给一个年份,就可以通过这个算法计算得到是否闰年的结果。
另外一种方法是,事先建立一个有2050个元素的数组,然后把所有的年份按下标的数字对应,如果是闰年,则此数组元素的值是1,如果不是元素的值则为0。这样,所谓的判断某一年是否为闰年就变成了查找这个数组某一个元素的值的问题。
第一种方法相比起第二种来说很明显非常节省空间,但每一次查询都需要经过一系列的计算才能知道是否为闰年。第二种方法虽然需要在内存里存储2050个元素的数组,但是每次查询只需要一次索引判断即可。
这就是通过一笔空间上的开销来换取计算时间开销的小技巧。到底哪一种方法好?其实还是要看你用在什么地方。
(例子来源链接:https://www.jianshu.com/p/88a1c8ed6254)
由此我们可以看出,算法的空间复杂度S(n)和时间复杂度T(n)本质上是有相互驳斥的,但在大多数情况下都优先考虑时间复杂度T(n),因为算法的空间复杂度S(n)大顶多计算机多分配给它多一些内存而已;但是时间就不一样了,有时候一个通过迭代的程序,可能跑了老半天!当然这个是相对而言的。
三、线性表:
线性表:零个或者多个数据元素的有限序列。
两种物理结构:顺序存储结构(顺序表)、链式存储结构(链表:单链表、静态链表、循环链表和双想链表)。
顺序表里面元素的地址是连续的,链表里面节点的地址不是连续的,是通过指针连起来的。
线性表的抽象数据类型定义如下:
ADT线性表(List)
Data
线性表的数据对象集合为{a1,a2,a3......an};每一个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素(也就说跟排队一个,一个接一个),元素与元素之前是一对一关系。
Operation
InitList(*L)//初始化,建立一个空的线性表L
ListEmpty(L)//若链表为空,返回true;否则返回false
ClearList(L)//清空线性表
GetElem(L,i,*e)//在线性表中L第i个元素值返回给e
LocateElem(L,e)//在线性表中查找与定值e相等的元素,如果有则返回1否则为0
ListInsert(*L,i,e)//在线性表中L第i个元素插入e
ListDelete(*L,i,*e)//删除线性表中L第i个元素,并用e返回其值
ListLength(L)//求线性表元素个数
Example顺序表:
#define MAXSIZE 20//存储空间初始化分配量
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];//数组存储数据元素,最大值为MAXSIZE
int length;//线性表当前长度
}SqList;
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define int Status
Status GetElem(SqList L,int i,ElemType *e)//获得线性表元素
{
if(L.length == 0||i<1||i>L.length)//如果线性表长度为0或者i>0(说明找不到这个元素)
return ERROR;
*e = L.data[i-1];
return OK ;
}
Status ListInsert(SqList *L,int i,ElemType e)//插入操作,在线性表中L第i个元素插入e
{
int k;
if(L->length = MAXSIZE)//线性表已经满了
return ERROR;
if(i<1||i>L->length+1//在线性表中找不到
return ERROR;
if(i<=L->length)//想要插入的元素
{
for(k = L->length-1;k>=i-1;k--)//在第i个元素后的所有元素都要往后挪一位
L->data[k+1] = L->data[k];
}
L->data[i-1] = e;//直接把i插在最后一位
L->length++;
return OK ;
}
Status ListDelete(SqList *L,int i,ElemType e)//删除线性表中L第i个元素,并用e返回其值
{
int k;
if(L->length = 0)//线性表为空
return ERROR;
if(i<1||i>L->length+1//删除选择的位置不准确
return ERROR;
if(i<=L->length)//想要删除的元素不是在表最后的位置
{
for(k = i;k<L_>length;k++)//在第i个元素后的所有元素都要往前挪一位
L->data[k-1] = L->data[k];
}
e= L->data[i-1] ;//把值返回给e
L->length--;
return e;
}
总结:
优点:可以快速的存取表中任意位置的元素
缺点:插入和删除操作需要挪动大量元素、当表的长度变化比较大时,难以确定存储空间的容量,容易造成存储空间的“碎片”。
在顺序表中,我们一般都是用数组来存储数据的元素和利用数组的次序按安排元素之间的关系的。但在链式结构中,我们除了还有存储数据元素外,还要存储它的后继元素的存储地址,所以经常用到指针。
Example单链表的读取,插入和删除:
typedef struct Node
{
ElemType data;//数据
struct Node *next;//指针
}Node;
typedef struct Node *LinkList;
Status GetElem(LinkList L,int i,ElemType *e)//单链表的读取,用e返回L中第i个元素的值
{
int j;
LinkList p;
p = L->next;//让p指向链表L的第一个结点
j = 1;
while(p&&j<i)
{
p = p->next;
++j;
}
if(!p || j>i)//说明第i个结点不存在
return ERROR;
*e = p->data;//取i个结点的数据
return OK;
}
Status ListInsert(SqList *L,int i,ElemType e)//插入操作,在第i个结点之前插入新的元素e,L的长度+1
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p&&j<i)//寻找i-1结点
{
p = p->next;
++j;
}
if(!p || j>i)//说明第i个结点不存在
return ERROR;
s = (LinkList)malloc(sizeof(Node));//生成一个新的结点
s->data = e;
s->next = p->next;//
p->next = s;//将s赋值给了p的后继
return OK;
}
Status ListDelete(SqList *L,int i,ElemType e)//删除线性表中L第i个结点,用e返回其值,L的长度减1
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next&&j<i)//寻找i-1结点
{
p = p->next;
++j;
}
if(!p || j>i)//说明第i个结点不存在
return ERROR;
q = p->next;//生成一个新的结点
p->next = q-next;//把q指向下一个数据给了p-next
*e = q->data;
free(q);//用e返回其值
return e;
}
Example单链表的整表创建和删除:
void CreateListHead(LinkList *L,int n)//整表创建表头
{
LinkList p;
int i;
*L = (LinkList)malloc(sizeof(Node));//L为整个线性表
(*L )->next = NULL;
for(i = 0;i<n;i++)
{
p= (LinkList)malloc(sizeof(Node));//生成头结点
p ->data = rand()%100+1;//结点赋值
p->next = (*L)->next;//将新结点指向下一结点
(*L)->next = p;//插入到表头
}
}
void CreateListTall(LinkList *L,int n)//整表创建其他元素
{
LinkList p,r;
int i;
*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;//结点赋值
r->next= p ;//尾部节点指向新结点
r = p;//当前新结点定义为表尾终端的结点
}
r->next = NULL;//当前链表结束,指向NULL
}
void ClearListTall(LinkList *L)//整表删除
{
LinkList p,q;
p = (*L)->next;//p指向第一个结点
while(p)//当p不为空时,也就是还没到表尾最后一个结点
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;//当前头指针指向NULL,意味着该表已空
return OK;
}
总结:
优点:在插入和删除结点的时候比顺序表有优势,因为有指针的指向,将指针指向下一个结点就行,不用对逐一对插入位置后面的元素进行操作;删除也是这样。
循环链表:
将单链表中终端结点的指针端改为指向头结点,使得整个单链表形成一个环。
双向循环链表:
在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior;//前驱指针
struct DulNode *next;//后继指针
}DulNode,*DuLinkList;
这里只讲一下怎么插入:新结点s插入到p,p->next两个之间
s->prior = p;//s 的前驱指向p
s->next = p->next;//s的后驱指向p-next
p->next ->prior = s;//把s赋值p->next的前驱
p->next = s;//把s赋值pt的后继
静态链表:
对于没有指针的编程语言,可以用数组替代指针,来描述链表。
Example:
#define MAXSIZE 100
typedef struct
{
ElemType data;
int cur;
}Component,StaticLinkList[MAXSIZE];
Status InitList(StaticLinkList space)//链表初始化
{
int i;
for(i = 0;i<MAXSIZE-1;i++)
{
space[i].cur = i+1;
}
space[MAXSIZE-1].cur = 0;//最后一个元素的cur用来存放第一个插入元素的下标,相当于头结点
}
int Malloc_SLL(StaticLink space)//
{
int i = space[0].cur;
if(space[0].cur)
{
space[0].cur = space[i].cur;
}
return i;
}
Status ListInsert(StaticLinkList L,int i,ElemType e)//在L中第i个元素插入新的数据元素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++)
k = L[k].cur;
L[j].cur = L[k].cur;
L[k].cur = j;
return OK;
}
rerurn ERROR;
}