在上一篇文章中我讲到了顺序表其主要优点是:
1.无需为表示节点的逻辑关系而增加额外的存储空间,存储利用率高。
2.支持随机访问,通过索引可以随机存取表中的任一元素,存取速度快。
缺点是:
1.在头部或者中间插入,删除元素时,为了保持其他元素的相对次序不变,需要对数据继续移动,运行效率很低。
2.由于顺序表要求地址空间必须连续,所以在申请空间的时候很难确定申请多大的空间才合适,申请多了浪费空间,申请少了不够用
首先链表在内存上不是连续的,我们只需要用指针来表示节点之间的逻辑关系,所以这就使得我们在插入,删除数据变得很简单,因此链表的存储节点包含两个部分,一部分用于存储需要存储的数据,另一部分存储指针,用于指向另外一个节点
在平时开发中我们使用单向不带头非循环和双向带头循环居多,而且只要掌握了这两种结构,其他的结构也能很轻松的实现
学数据结构一定一定一定要动手画,不然会很难学明白!!!
学数据结构一定一定一定要动手画,不然会很难学明白!!!
学数据结构一定一定一定要动手画,不然会很难学明白!!!
重要的事情说三篇!
typedef int anytype; //若要存储其他数据类型更改这里的int为你想要储存的数据类型即可
typedef struct STL_list //节点定义
{
anytype data;
struct STL_list* next;
}list;
首先定义一个指向节点类型的一个指针phead,当链表不存在时,让该指针指向空,当链表存在时,该指向必须指向该链表的第一个节点,后面对链表的所有操作,增,删,查,改都要需要用这个指针找到链表的头节点才能进行相应的操作
int main(){
list* phead = NULL;
push_back(&phead,1);//尾插
push_front(&phead,2);//头插
..................
return 0;
}
因为不管是头插还是尾插,或者是中间插入,我们都要先申请一块内存空间,创建节点,所以为了提高代码的复用性,以及减少重复代码,我们将创建节点单独拿出来封装成一个函数
list* add_newspace(anytype x) { //创建成功返回节点指针
list* newspace = (list*)malloc(sizeof(list));//成功返回指向该节点的指针,失败返回NULL
if (newspace != NULL) {
newspace->data = x; //将要储存的数据x放到节点的数据域data中
newspace->next = NULL; //初始化next指针,让他指向空
return newspace;
}
else{
printf("节点创建失败!!");
exit(-1); //创建失败,退出程序
}
}
尾插原理:如果当前链表为空,就让插入的第一个元素当链表头节点,因为第一个节点插入之后,此时链表只有一个节点,所以这个节点既是头节点也是尾节点,要让该节点的next指向NULL
如果当前链表不为空,我们就去找到链表的最后一个节点,让链表的最后一个节点指向我们要插入的节点即可
void push_back(list** phead, anytype x){
list* newspace = add_newspace(x); //调用函数创建节点
if (*phead == NULL) { //如果*phead等于空,说明链表为空,我们需要让*phead指向该节点即可
*phead = newspace;
}
else { //链表不为空就要去找他的最后一个节点
list* cur = *phead; /*因为*phead要么指向空,要么就指向链表第一个节点,所以一旦*phead
指向第一个节点之后我们就不能移动他,所以我们将*phead保存到cur中,我们去移动cur找到最后
一个节点*/
while (cur->next != NULL) { //只要cur->next!=null,就不是最后一个节点
cur = cur->next; //将cur指针向后移动一位,指向下个节点
}
cur->next = newspace; //找到最后一个节点,让最后一个节点的next指针指向要插入的节点
}
}
头插原理:头插比尾插简单,因为不管链表是否为空我们都要把新节点放到第一个位置,所以不用考虑链表为空的情况,来一个插入一个,然后让*phead一直保持指向第一个节点即可
void push_front(list** phead, anytype x) {
list* newspace = add_newspace(x);
if (*phead == NULL) {
*phead = newspace;
}
else {
newspace->next = *phead; //链表不为空,让要插入的节点指向第一个节点
*phead = newspace; //然后再让phead指针指向这个新节点
}
}
尾删原理:要先找最后一个节点和最后一个元素的前一个节点,才能把最后一个删掉,因为倒数第二个节点的next指针是指向最后一个节点的,如果直接删掉最后一个节点,不把倒数第二个节点置空的话,这就是一个典型的野指针案列,所有我们必须要找到倒数第二个节点,把最后一个节点删除之后,把倒数第二个节点的next指针置空,这样才是一个完整的尾删过程
void pop_back(list** phead) {
if (*phead == NULL) { //链表为空直接返回
return;
}
else if ((*phead)->next==NULL) { //说明只有一个节点
free(*phead); //直接删除
*phead = NULL;
}
else { //说明有多个节点,找到最后一个节点和倒数第二个节点再删除
list* cur = *phead; //因为*phead必须指向头节点,所以把*phead保存到cur,用cur找尾节点
list* ret = NULL; //用ret保存倒数第二个节点
while (cur->next != NULL) {
ret = cur; //cur每向后面走一步之前,都先把当前指针保存到ret,不断更新ret指针
cur = cur->next; //当cur走到最后一个节点时,ret一定是倒数第二个节点
}
if (ret != NULL) {
ret->next = NULL;
}
free(cur);
}
}
头删就比较简单了,如果链表为空,直接退出,不为空,先将指向第一个节点的指针保存到cur,然后把指向第一个节点的指针向后移动一位,指向第二个节点,再删除第一个节点即可
void pop_front(list** phead) {
if (*phead == NULL) {
return;
}
else {
list* cur = *phead; //保存头节点指针
*phead = (*phead)->next; //移动指向头节点的指针,让第二个节点成为新的头节点
free(cur);
}
}
list* find(list* phead, anytype x) { //成功返回指向储存该元素节点的指针,失败返回NULL
if (phead == NULL) {
return NULL;
}
else {
while (phead != NULL) //遍历一次链表,找到返回指针,找不到返回NULL
{
if (phead->data == x) {
return phead;
}
else {
phead = phead->next;
}
}
return NULL;
}
}
原理也很简单,我们需要考虑两种情况
一:当链表为空时,或者指定元素不存在时,直接返回
二:当链表不为空时,我们要考虑三个因素,删的是第一个节点,还是中间节点,还是最后一个节点
void delete_anywhere(list** phead , anytype x) {
if (*phead == NULL) { //链表为空,直接返回
return;
}
else {
list* cur = *phead; //和上面一样,把指向头节点的指针保存到cur
list* ret = NULL; //ret指向cur的前一个节点,ret最开始指向NULL,cur最开始指向第一个节点
while (cur != NULL) { //循环遍历的去找哪一个节点的data与x相等
if (cur->data == x) { //如果找到了,直接退出循环
break;
}
ret = cur; //如果没有找到,ret和cur都向前走一步,指向下一个节点,再进行比较
cur = cur->next;
}
if (ret != NULL && cur != NULL) { //如果这里判断条件成立,说明在中间或者最后找到了
ret->next = cur->next; //把要被删除节点的前一个节点和后一个节点连接起来
free(cur); //删除节点
}
else {
if (cur != NULL) { //如果这里条件判断成立,说明找到了,而且就是第一个节点
*phead = cur->next; //既然删除的是第一个节点,那么就要更新头指针的指向
free(cur);
}
}
}
}
清空链表就没有什么可讲的了,方法有很多,我这里是用头删的思路来的,头删是删除一个节点,那么清空我就循环进行头删,直到链表为空为止
void Destroy(list** phead) {
if (*phead == NULL) {
return;
}
else {
while (*phead != NULL) {
list* cur = *phead;
*phead = (*phead)->next;
free(cur);
}
}
}
这个可写可不写,主要是为了方面测试,循环一个一个把数据打印出来就行
void my_printf(list** phead) {
if (*phead == NULL) {
printf("链表为空!");
}
else {
list* cur = *phead;
while (cur != NULL) {
printf("%d ", cur->data);
cur = cur->next;
}
}
}
#pragma once
#include
#include
typedef int anytype;
typedef struct STL_list //节点定义
{
anytype data;
struct STL_list* next;
}list;
//接口函数定义
void push_back(list** phead, anytype x); //尾插
list* add_newspace(anytype x); //创建一个新节点并返回该节点的指针
void my_printf(list** phead); //打印链表
void push_front(list** phead, anytype x); //头插
void pop_back(list** phead); //尾删
void pop_front(list** phead); //头删
list* find(list* phead, anytype x); //查找指定元素并指向该节点的指针
void Destroy(list** pphead); //清空链表
list* add_newspace(anytype x) {
list* newspace = (list*)malloc(sizeof(list));
if (newspace != NULL) {
newspace->data = x;
newspace->next = NULL;
return newspace;
}
}
void push_back(list** phead, anytype x) {
list* newspace = add_newspace(x);
if (*phead == NULL) {
*phead = newspace;
}
else {
list* cur = *phead;
while (cur->next != NULL) {
cur = cur->next;
}
cur->next = newspace;
}
}
void push_front(list** phead, anytype x) {
list* newspace = add_newspace(x);
if (*phead == NULL) {
*phead = newspace;
}
else {
newspace->next = *phead;
*phead = newspace;
}
}
void pop_back(list** phead) {
if (*phead == NULL) {
return;
}
else if ((*phead)->next == NULL) {
free(*phead);
*phead = NULL;
}
else {
list* cur = *phead;
list* ret = NULL;
while (cur->next != NULL) {
ret = cur;
cur = cur->next;
}
if (ret != NULL) {
ret->next = NULL;
}
free(cur);
}
}
void pop_front(list** phead) {
if (*phead == NULL) {
return;
}
else {
list* cur = *phead;
*phead = (*phead)->next;
free(cur);
}
}
void delete_anywhere(list** phead , anytype x) {
if (*phead == NULL) {
return;
}
else {
list* cur = *phead;
list* ret = NULL;
while (cur != NULL) {
if (cur->data == x) {
break;
}
ret = cur;
cur = cur->next;
}
if (ret != NULL && cur != NULL) {
ret->next = cur->next;
free(cur);
}
else {
if (cur != NULL) {
*phead = cur->next;
free(cur);
}
}
}
}
list* find(list* phead, anytype x) {
if (phead == NULL) {
return NULL;
}
else {
while (phead != NULL)
{
if (phead->data == x) {
return phead;
}
else {
phead = phead->next;
}
}
return NULL;
}
}
void Destroy(list** phead) {
if (*phead == NULL) {
return;
}
else {
while (*phead != NULL) {
list* cur = *phead;
*phead = (*phead)->next;
free(cur);
}
}
}
void my_printf(list** phead) {
if (*phead == NULL) {
printf("链表为空!");
}
else {
list* cur = *phead;
while (cur != NULL) {
printf("%d ", cur->data);
cur = cur->next;
}
}
}
int main()
{
list* phead = NULL;
for (int i = 0; i < 10; i++) {
push_back(&phead,i);
}
my_printf(&phead);
delete_anywhere(&phead,45);
printf("\n**********************************************\n");
my_printf(&phead);
return 0;
}
这个题和上面说到的在任意位置删除节点几乎一样,唯独不一样的地方就是,他不需要你真正的删除节点,只需要我们移动一下各个指针的指向就行,所以上面的代码完全可以复用,把删除节点的几行注释掉,再把结构体名字和头指针名字一换,再对应题目要求返回相应指针就行了
这里代码和上面删除任意函数节点的代码一样,如果有不理解的地方可以倒回去看看
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
if (head == NULL) {
return NULL;
}
else {
ListNode* cur = head;
ListNode* ret = NULL;
while (cur != NULL) {
if (cur->val == val) {
break;
}
ret = cur;
cur = cur->next;
}
if (ret != NULL && cur != NULL) {
ret->next = cur->next;
//free(cur);
}
else {
if (cur != NULL) {
head = cur->next;
//free(cur);
}
else{
return cur->next;
}
}
}
return head;
}
};
我以前犯过一个错误,就是在刚开始学数据结构的时候,我总是拿顺序表和链表做对比,因为他们实现的都是一个东西,就是线性表,我的想法是既然都是为了描述线性表这种数据结构那为什么要把两个都学精学懂,会一个不就好了吗?我带着这样一种心态去学习,结果可想而知,既没学懂,还浪费很多了时间,而且学习过程很痛苦。
所以我想告诉大家,这些数据结构没有最好的,只有最合适的,针对不同的场景,使用不同的数据结构,随着你更加深入的学习,你就会知道每一种数据结构都是不可缺少的。
好了,今天就写到这里了,下篇讲双向带头循环链表。