#小白一次就可以看懂的数据结构
学习目录
1. 线性表
当我们第一次接触到数据结构时,我们不禁会问什么是线性表,什么是顺序表,什么又是单链表?我相信很多人都和我一样刚开始的时候都一头雾水。现在让我们简单的介绍一下上述的这些概念。
首先,线性表可以看成是由一个一个小朋友手牵着手排成一列的情况,一个队头一个队尾,除此之外每一个小朋友都分别知道他前面和后面的小朋友是谁。这样的一个结构就可以称作是线性表。
1.1 线性表的分类以及抽象数据结构
线性表一般可以分为顺序存储结构与链式存储结构。其中顺序存储结构可以叫作顺序表,链式存储结构叫作单链表(具体其他的请看下一节)。
typedef int Elemetype; //Elemtype取决于用户真实的类型,这里定义为int
typedef struct
{
Elemtype data[size]; //size指的是数据的大小
int length;
}SqList;
//定义一个顺序表
SqList *q;
假如我们要求顺序表中的一个元素呢?别担心,有一条公式可以帮助我们解决问题,
假设第一个元素的位置为LOC(a1),每个元素占用d个字节,那么第i个所处地址为:
1.2 线性表的插入与删除
线性表的插入操作是指在表的第i个位置上插入一个值为x的新元素,插入后使原长度为n的表变为n+1。
算法思路:如果插入的位置不合适,将会提出异常;从最后一个元素想前面遍历到第k-1个元素,随后让他们的位置每个都往后移动一位,最后将插入的元素插入到第k个元素里。
接下来请看代码:
# define False 0;
# define TRUE 1;
# define ERROR 0;
int Insertdata(SqList *q,int i,Elemtype e)
{
int k;
if(q->length==size)
{
printf("表满");
return False;
}
else if(i<1 || i>q->length+1)
{ //当d不在范围内的时
return ERROR;
}
else if(i<=q->length)
{ //当i不处于表尾时
for(k = q->length-1;k>=i-1;k--){
q->data[k+1] = q->data[k]; //将要插入位置后的元素往后移动一位
}
}
q->data[k-1] = e;
//因为上面循环最后一个元素的坐标是data[k-1]
//但是因为k+1后变为了data[k],所以data[k-1]的位置没有被占用;
q->length++;
return TRUE;
}
删除操作的原理:如果删除的位置不合适,将会提出异常;如果在合理范围内,那么先显示要删除的元素,随后从此位置开始往后遍历,让后面的元素往前移动一位。
接下来让我们看一下代码:
int Deletedata(SqList *q,int i,Elemtype *e)
{
int k;
if(q->length==0) return ERROR;
*e = q->data[i-1];
if(i<q->length)
{
for(k = i;k<q->length;k++)
{
q->data[k] = q->data[k+1]; //第k个元素将会被后面的元素所覆盖,也就是向前移动一位
}
}
q->length--;
return TRUE;
}
1.3 单链表的抽象数据结构
前面我们讲的是顺序表(即顺序存储结构),但是他有一个缺点–插入与删除数据时需要移动大量的元素,这将耗费大量的时间。那么有没有一种形式可以让我们可以快速实现上述操作的同时还不需要担心data[size]的范围呢?有的,这就是我们接下来要讲的链式存储结构–单链表。
首先我们在这里定义存储数据的地方叫做数据域,它的后继位置叫做指针域(用来指明下一个元素的位置在哪里)。这两个部分我们可以将它定义为节点(Node)。
typedef struct LinkList
{
Elemtype data; //数据域
struct LinkList *next; //指针域
}Node,*List;
//头指针的变量定义:
List head;
//算法中要用到的指向新数据的变量
Node *p;
p = (Node*)malloc(sizeof(Node)); //为新的节点申请一片Node类型的空间
1.4 单链表的插入与删除
在熟练运用单链表进行相应操作时,我们要首先掌握几种基本的操作。首先,我们要怎么将一个新的数据插入到链表中呢?
//插入数据创建单链表
List create_list()
{
List p = (List*)malloc(sizeof(List)); //创建头节点
p->next = NULL;
Node *newnode; //生成一个新节点
int x; //要插入的数据
scanf("%d",&x);
while(x!=-1)
{
newnode = (Node*)malloc(sizeof(Node)); //为新节点开辟空间
newnode->data = x;
newnode->next = p->next;
p->next = newnode;
scnaf("%d",&x);
}
return p;
}
看到这里,可能有不少的小伙伴对于这其中的代码存在着些许疑惑:为什么在将x赋值给新节点后,要进行后面的两步代码呢?没事,我们慢慢解释:
newnode->next = head->next;
head->next = newnode;
我们要想要插入一个新数据,根本用不着去改变其他部分的代码。第一句代码是将p的后继节点改为newnode的后继节点(原本p指向的是p->next,现在是由newnode指向p->next)
再把新节点newnode改为p的后继节点(现在p指向的是newnode)
看到这里,可能有的小伙伴会问,为什么这里插入要从头节点插入,从尾部插入不行吗?我的回答是,当然可以,为什么不行。接下来引出我们插入数据的第二种方法:尾插法
//对于表头与表尾的特殊情况,其实做法是一样的
int createlist()
{
List p = (List*)malloc(sizeof(List)); //生成头节点
p->head = NULL;
Node *newnode,*rear = p;
int x;
scanf("%d",&x);
while(x!=-1)
{
newnode = (Node*)malloc(sizeof(Node));
newnode->data = x;
rear->next = newnode;
rear = newnode;
scanf("%d",&x);
}
rear->next = NULL;
return p;
}
还是同样的,让我们解释rear->next = s; rear = s;这两句代码。
第一句代码:我们将新节点放到链表尾部节点rear->next(让rear节点后继指向新节点)
第二条代码:随后我们让rear节点移动到新的节点上(如果不移动的话它还在原来的位置)
接下来进行我们的最后一个操作:删除链表
int Deletedata(List *L)
{
List p,q;
p = *L->head;
while(p)
{
q = p->next; //将q赋值为q的下一个节点
free(p); //释放当前的节点
q = p; //让q等于它的下一个节点
}
*L->next = NULL;
return TRUE;
}
1.5 单链表与顺序表的优缺点
好了,说了这么多,让我们来总结一下:
顺序表的优点是存取速度高效,通过下标来直接存取;缺点是插入和删除比较慢,不可以实时增长长度。适用于需要大量访问元素的,而增加/删除元素较少的程序。
单链表的优点是插入与删除的时间复杂度为O(1),不需要分配额外的存储空间,也不需要担心节点的数量是否超过数据的大小,适用于数据大小不确定的情况。缺点是CPU高速缓存效率低。