时间:2019年6月23日20:00:31
前面已经说到了,学习数据结构的目的是在程序设计中能够正确的选择合适的数据结构,让数据能够高效的存储和使用
对于我个人而言,数据结构和算法这门课如果细分的话,数据结构是告诉我们数据在逻辑上的几种存储方式,我们要明白他的原理以及他的应用,而算法是对已经存在的数据进行操作。
1. 线性表
1.1 什么是线性表?
线性表顾名思义,它就是像线一样性质的表。是最常用最简单的一种数据结构。
简言之,一个线性表是n个数据元素的有限序列。至于每个数据元素的具体含义,在不同的情况下各不相同。
1.2 线性表的定义
(1)、存在唯一的一个被称做“第一个”的数据元素;
(2)、存在唯一的一个被称做“最后一个”的数据元素;
(3)、除第一个之外,集合中的每个数据元素均只有一个前驱;
(4)、除最后一个之外,集合中每个数据元素只有一个后继;
1.3 线性表的物理结构
一、顺序的存储
二、链式的存储
2.线性表的顺序存储
2.1 什么是线性表的顺序存储?
我前面已经说过线性表就是一根线,也就是说他的存储结构是一条直线样式的,那么线性表的顺序存储它从物理上将一根直线从头到尾中间不能带有一点分叉。
举个例子,就像军训的时候排队去领军装,那么这个队伍只有一个头部,最后也只有一个尾部,一个学生连着一个学生,中间不能有空隙,毕竟要是有空隙老师就会骂的。
在计算机中线性表的顺序存储就是用一段地址连续的存储单元依次存储线性表的数据元素
比如:int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; a就是一段线性表,它指向了这个线性表的头部。
2.2 顺序存储的线性表的一系列算法
- 2.2.1 线性表顺序存储的创建
- 首先我们来分析一下如何创建一个线性表,首先我们知道不管是任何指针类型的变量在内存中只占4个字节,那我们可以不管上层要插入什么类型的元素,我们只需要记住他们插入变量的地址,然后组成一个线性表即可,也就是说,我们不管插入元素变量的生命周期。
- 我们应该需要记录一下上层要让我们给他创建多大的线性表,也就是线性表的长度,然后我们还需要记录一下这个数组中有效元素的个数。(我们虽然不知道上层要让我们给他创建多大的数组,但是我们必须知道有几个有效的元素,这样方便我们进行操作)
typedef void List;
typedef void ListNode;
typedef struct seqList
{
int capacity; //记录上层要我们开辟内存的容量
int len; //记录这个数组中有多少个有效元素个数
unsing int * node; //数组的首地址
}TList;
List * List_Create(int capacity)
{
TList * temp = (TList *)malloc(sizeof(TList));
if(temp == NULL)
{
printf("线性表创建失败");
return NULL;
memmset(temp, 0, sizeof(TList));
temp->node = (unsing int *)malloc(sizeof(unsing int *) * capacity);
if(NULL == temp->node)
{
printf("线性表创建失败 node malloc");
return NULL;
}
temp->len = 0;
return temp;
}
- 2.2.2 线性表顺序存储的在某一个位置插入一个元素
- 首先我们已经创建了一个顺序存储的线性表,也明白了它的原理,那么如何从线性表中插入一个元素呢
比如说我们从下标为3(pos)的数组中插入一个元素,我们先把流程搞清楚,让数组中最后一个有效元素往后移,将下标位置为3的空出来,之后我们就可以加入了,
int List_Insert(List * list, ListNode * node, int pos)
{
if(list == NULL || node == NULL || pos < 0)
{
printf("list == NULL || node == NULL || pos < 0 error");
return -1;
}
TList * temp = (TList *)list;
int i = 0;
if(temp->node != NULL)
{
for(i=temp->len; i>pos; i--)
{
temp->node[i] = temp->node[i-1];
}
temp->node[i] = (unsing int *)node;
temp->len++;
return 0;
}
}
- 2.2.3 线性表顺序存储的销毁
这里销毁一个线性表的顺序存储是把list列表内存释放掉
void List_Destroy(List * list)
{
if(list == NULL)
{
printf("list == NULL");
return;
}
TList * temp = (TList *)list;
if(temp->node != NULL)
{
free(temp->node);
}
free(temp);
}
- 2.2.4 线性表顺序存储的恢复默认装填
这里指的是讲一个线性表的所有值恢复创建的时候
void List_Clear(List * list)
{
if(list == NULL)
{
printf("list == NULL");
return;
}
TList * temp = (TList *)list;
temp->len = 0;
}
- 2.2.5 线性表顺序存储删除某一个元素
从线性表顺序存储中指定位置删除一个元素的话,那么就是数组中的地址前移了
上流程,比如果删除这个数组中的3也就是下标为2的元素,那么就是从下标为3之后依次往前移动
//删除某一个元素
ListNode* LIst_DeleteNode(List* list, int pos)
{
int i = 0;
if (list == NULL || pos < 0)
{
printf("list == null || pos < 0");
return NULL;
}
PList* temp = (PList*)list;
if (pos > temp->size)
{
printf("pos > temp_>size");
return NULL;
}
ListNode* pls = temp->node[pos-1];
//元素前移
for (i=pos-1; i<temp->lenth; i++)
{
temp->node[i] = temp->node[i + 1];
}
temp->node[temp->lenth - 1] = 0;
temp->lenth--;
return pls;
}
线性表的优缺点总结:
优点:
1)、无须为表示表中逻辑关系而增加额外的存储空间
2)、可以快速的存取表中任意位置的元素
缺点:
1)、插入删除需要移动大量的元素
2)、当线性表长度变化较大的时候,难以确定存储空间的容量
3 线性表链式存储
前面已经说到了线性表的顺序存储,其实它就是一个数组,我们知道数组就是内存中开辟一块连续的内存空间,那么问题来了,如果我存储的东西非常大,用数组还行吗?肯定不行的,所以我们这次来学习链表。
3.1 什么是链式存储?
链式存储其实就是链表。
首先我们来看一下链表的发展形式
首先我们在学习C语言的时候,比如写一个学生管理系统,那肯定要用结构体来封装学生的信息(姓名、年龄、学号。。。)前面我们已经说了,内存是线性的,如果我们直接使用数组来存储一个班级中的学生所有信息,如果人数多的话,那肯定是不行的,这个时候我们就要用到链表,链表就是在学生类型中加一个指向它自己类型的指针,这是传统链表。
因为传统链表是包含下一个节点的地址,那么如果这个节点增加了一个成员。那么这个链表就要重新实现。
还有一点,因为我们要写成的链表程序,是让别人调用的api所以我们不能知道他们要插入的是什么数据类型,所以我们用到第三种类型的链表来编写。
3.2 链表的种类
3.2.1 单向链表
单向链表就是从名字上可以看出他是单向的,也就是说它指向了下一个节点的地址。
linklist.h
1. 我们不知道上层写的节点是什么数据类型的所以我们要对void 类型 重命名
2. 从上面图中我们可以分析到,节点包含着下一个节点的地址。所以上层在调用我们写的api的时候,他定义的节点要包含我们写的一个结构体(结构体成员指向下一个他们身类型的指针)
typedef void LinkList;
typedef struct _tag_LinkListNode
{
struct _tag_LinkListNode * next;
}LinkListNode;
//创建单向链表
LinkList * LinkList_Create();
//销毁一个链表
void LinkList_Destroy(LinkList * list);
//让链表恢复到初始化状态
void LinkList_Clear(LinkList * list);
//获取链表的长度
int LinkList_Length(LinkList * list);
//指定位置插入
int LinkList_Insert(LinkList * list, LinkListNode * node, int pos);
//通过下标获取元素
LinkListNode * LinkList_Get(LinkList * list, int pos);
//删除指定位置元素
LinkListNode * LinkList_Delete(LinkList * list, int pos);
linklist.c
1. 上面我们已经定义了一个链表的节点,那么我们需要考虑一下,我们的任务是将一个
一个的节点串连起来,那么我们需要记录一下链表的有效个数。然后链表的节点
typedef struct _tag_LinkList
{
LinkListNode header;
int len;
}TLinkLsit;
//链表的创建,我们创建的链表是带有头结点的链表
//创建单向链表
LinkList * LinkList_Create()
{
TLinkLsit * temp = (TLinkList *)malloc(siezoef(TLinkList));
memset(temp, 0 sizeof(TLinkList));
return temp;
}
//销毁一个链表
void LinkList_Destroy(LinkList * list)
{
首先我们是创建了一个带有头结点的链表,他的herad是指向链表的首节点的。因为
我们写的是一个底层库。上层节点的声明周期我们不需要管理,所以我们只需要释
放头结点的内存就可以了
if(list != NULL)
{
free(list);
}
}
//让链表恢复到初始化状态
void LinkList_Clear(LinkList * list)
{
只需要将TLinkList 中 header中节点指向NULL
len清空即可
if(list != NULL)
{
TLinkList * temp = (TLinkList *)list;
temp->header.next = NULL:
temp->len = 0;
return;
}
printf("list == NULL error ");
return ;
}
//获取链表的长度
int LinkList_Length(LinkList * list)
{
if(list == NULL)
{
printf("list == null");
return -1;
}
TLinkList * temp = (TLinkList *)list;
return temp->len;
}
//指定位置插入
int LinkList_Insert(LinkList * list, LinkListNode * node, int pos)
{
if(list == NULL || node == NULL || pos < 0)
return -1;
int i = 0;
TLinkList * temp= (TLinkList *)list;
LinkListNode * current = &(temp->header);
for(i=0; i<pos && pos < len; i++)
{
current = current->next;
}
node->next = current->next;
current->next = node;
temp->len++;
return 0;
}
//通过下标获取元素
LinkListNode * LinkList_Get(LinkList * list, int pos)
{
if(list == NULL || pos < 0)
return NULL;
TLinkList * temp = (TLinkList *)list;
LinkListNode * current = &(temp->header);
int i = 0;
for(i=0; i<pos; i++)
{
current = current->next;
}
return current;
}
//删除指定位置元素,返回删除元素
LinkListNode * LinkList_Delete(LinkList * list, int pos)
{
if(list == NULL || pos < 0)
return NULL;
TLinkList * temp = (TLinkList *)list;
LinkListNode * current = &(temp->header);
LinkListNode * ret = NULL;
int i = 0;
for(i=0; i<pos; i++)
{
current = current->next;
}
ret = current->next;
current->next = ret->next;
temp->len--;
return ret;
}
插入:
首先我们要把NULL想成一个节点,而不是说头结点next域里是NULL。然后我们插入的话,第一步是让最下面的节点指向下一个节点,而下一个节点的地址存在temp的next中 所以我们要让下一个节点的next 赋值为temp->next 然后再连接temp->next = 下一个节点的地址
删除:
3.2.2 单向循环链表
循环链表就是链表的最后一个节点指向第一个节点
我就不写代码了,可以在每次插入新元素的时候,获取最后一个元素,让最后一个元素指向首节点。
3.2.3 双向链表
一些简单的操作我就不写了。先来说一下特殊情况。首先在中间插入节点是没有任何问题的。问题出现在头节点的前面插入或尾结点的后边插入就会出现异常。
插入:
其实在中间插入是非常简单的,主要是异常,当第三个辅助指针变量指向NULL的处理 删除也是,所以我直接把代码贴出来 其实处理这些也是非常简单的,就是判断ret是不是为NULL
//插入元素
int List_Insert(TLinkList* list, TLinkList* node, int pos)
{
int i = 0;
Plist* temp = (Plist*)list;
TLinkList* current = &(temp->node);
TLinkList* ret = NULL;
if (list == NULL || node == NULL || pos < 0)
{
printf("list == NULL || node == NULL || pos > 0 err\n");
return -1;
}
for (i=0; i<pos; i++)
{
current = current->next;
}
ret = current->next;
node->next = ret;
current->next = node;
if (ret != NULL) //通过这里来判断是不是头一次或者尾部插入
{
ret->pro = node;
}
node->pro = current;
temp->len++;
return 0;
}
删除:
//删除元素
TLinkList* List_DeleteNode(TLinkList* list, int pos)
{
int i = 0, z = 0;
if (list == NULL || pos < 0)
{
printf("list == NULL || pos < 0 error \n");
return NULL;
}
Plist* temp = (Plist*)list;
TLinkList* current = &(temp->node);
TLinkList* ret = NULL;
TLinkList* ptem = NULL;
for (i=0; i<pos - 1; i++)
{
current = current->next;
z++;
}
ptem = current->next;
current->next = ptem->next;
if (ptem->next != NULL)
{
ret = ptem->next;
current->next = ret;
ret->pro = current;
}
temp->len--;
return ptem;
}