数据结构-----链表

        数据结构是计算机存储、组织数据的方式。顺序表底层就是数组。顺序表(物理结构和逻辑结构都连续)是线性表(具有相同特性的数据结构的集合;物理结构不一定连续;逻辑结构一定连续)的一种。

        常见的线性表:顺序表、链表、栈、队列、字符串..... 线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

一、

···数组的分类:

int arr[10]={0};定长的数组

int* arr;动态内存开辟,确定大小之后再去动态申请

···顺序表分类:

¥静态顺序表的定义:(底层是定长数组,已经确定大小)

坏处:数组大小给小了导致空间不够用,数组大小给打了导致空间浪费。

一般将所开辟的数组大小设置为宏定义,方便后续的改动。      #define N 100

¥动态顺序表的定义:

可以进行动态增容,可以更好的把控空间大小。

我们不知道顺序表中存储的数据类型是什么,所以给数据类型重定义typedef

注意结构体重命名的写法

二、单链表各种操作

//顺序表不为空有两种情况:(需要特别注意这种情况的是当需要尾删的时候)
//1.传参传的地址是NULL
//2.顺序表有地址即顺序表存在,但是其size为0,当访问下标的时候要size-1,此时就完蛋了

//capacity/size  ++/--

//在指定位置pos之前插入数据
//那么pos>=0&&pos<=size
//最之前可以在以一个数据之前插入即第0个位置
//最之后即可以在最后一个数据的下一个位置之前插入,即实现尾插
//这个断言同时也可以拦截顺序表为空的第二种情况

//对通讯录操作实际上就是对顺序表进行操作,只是把结构体重命名作为顺序表中一个空间的ElemType了
//顺序表头文件要用到通讯录头文件的struct personInfo,而Contact.h要用到SeqList.h的SL,不能互相include
//不能互相包含头文件,交叉使用头文件会导致程序报错
//这里要用到前置声明
//可以写成typedef struct SeqList Contact;//这一步就叫做前置声明
//不可以写成typedef SL Contact;因为在Contact.h文件中没有包含SeqList.h,只能使用最原始的名字;如果包含了就能使用包含的那个头文件中typedef之后的名字了
//在SeqList.h文件中虽然使用了typedef进行了重命名,但是是先定义然后改的名字
//在Contact.c文件中肯定会用到Contact.h中的内容,这个肯定要包含,同时,因为对通讯录操作就是对顺序表进行操作,可以节省一些步骤,所以也要包含SeqList.h文件

//使用o(1)复杂度的程序,删除数组中所有的等于value的值
//也就是说不能再开辟一块空间
//双指针法:创建两个指针变量src、dst同时指向起始位置
//  若src指向的值是value,则src++
//  若src指向的值不是value,则nums[dst]=nums[src],src++,dst++
//  最后src指向结尾,dst移动的次数(dst指向的坐标位置)就是当前数组中还剩多少个数据
int removeElement(int* nums,int numsSize,int val)
{
    int src,dst;
    src=0,dst=0;
    while(src<numSize)
    {
        if(nums[src]==val)
        {
            src++;
        }
        else
        {
            nums[dst]=nums[src];
            src++;
            dst++;
        }
    }
    return dst;//返回新数组的有效长度
}

//将nums1和nums2两个非递减的有序数组合并到nums1这一个数组中
//方法一:
//将nums2中的数据先都放在nums1的后面,然后对nums1进行排序
//方法二:
//三个指针,两个分别指向两个数组的最后一个有效元素,另一个指针指向合并后nums1的最后一个位置
//两个指针处的数据比较大小,谁大谁往第三个指针那里放,然后指针减减,当有指针为0时结束循环
//同时注意:不可能两个指针同时为0,因为一次只挪动一个数据,必定有一个先移完
//结束循环之后,如果nums1中的那个指针不为0不用管,因为nums1本身有序
//如果nums2中的指针不为0,需要将他放到nums1中的第三个指针里
void merge(int* nums1,int m,int* nums2,int n)//m和n分别为nums1和nums2的初始有效个数
{
    int l1=m-1;
    int l2=n-1;
    int l3=m+n-1;
    while(l1>=0&&l2>=0)
    {
        if(nums1[l1]>nums2[l2])
        {
            nums1[l3--]=nums1[l1--];
        }
        else
        {
            nums1[l3--]=nums2[l2--];
        }
    }
    while(l2>=0)
    {
        nums1[l3]=nums2[l2--];
    }
}

//在单链表中插入一个节点时,需要注意如果链表是空链表,那么链表明就是空指针,而空指针不能解引用
//即SLTNode* ptail=phead(被传参的链表);//这样是会报错的
//所以当为空链表的时候,应该让ptail直接指向新的节点
//如果不是空链表就循环找要插入那个点,然后改变指针指向

SLTNode* plist=NULL;
SLTPushBack(plist,1);
void SLTPushBack(SLTNode* phead,SLTDataType x);
//形参的改变要影响实参的话必须要传地址
//这样传参会导致只有在被调用的函数中改变了,除了被调函数的作用域就再恢复原来的状态
//因为要改变的是plist的内容,虽然plist存储的内容是指针,但是只有取地址才能实现地址传参
//才能真正改变plist的内容,即才能实现改变plist指向的空间
//要写成
SLTNode* plist=NULL;
SLTPushBack(&plist,1);//对一级指针取地址就要用二级指针来接收
void SLTPushBack(SLTNode** pphead,SLTDataType x);
//该处pphead不能为NULL,因为后面要对他进行解引用来用指针指向的节点来找位置
//而*pphead可以为空,此处代表pphead的内容为空,即pphead指向NULL
//*plist(解引用) 第一个节点         =**pphead(对*pphead解引用)
//plist 指向第一个节点的指针        =*pphead(对pphead解引用)
//&plist 指向第一个节点的指针的地址 =pphead(形参与实参相等)
//删除节点时,pphead和*pphead都不能为空,因为空指针不能解引用和为空删不了

//链表尾删
void SLTPopBack(SLTNode** pphead)
{
    //链表不为空
    assert(pphead&&*pphead);
    //链表只有一个节点
    if((*pphead)->next==NULL)//->的优先级高于*
    {
        free(*pphead);
        *pphead=NULL;
    }
    //链表有多个节点
    else
    {
        SLTNode* prev=*pphead;
        SLTNode* ptail=*pphead;
        while(ptail->next)
        {
            prev=ptail;
            ptail=ptail->next;
        }
        free(ptail);
        ptail=NULL;
        prev->next=NULL;//当只有一个结点的时候,这一个节点被释放了,就不能再用next来指向下一个了,会非法访问
    }
}

//链表头删
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead&&*pphead);
    //只有一个结点的时候也行,因为那一个结点的下一个是NULL
    SLTNode* next=(*pphead)->next;
    free(*pphead);
    *pphead=next;
}

//当指向链表头部的指针可能发生变化时,传参的链表名就需要使用二级指针

//在指定位置之前插入数据
//先找到那个数据,即找到指向某个节点的指针
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{
    assert(pphead&&*pphead);
    assert(pos);
    //空指针不可以解引用
    //当链表为空时,对应的pos也为空,那么就找不到到底在哪个位置之前插入数据
    SLTNode* newnode=SLTBuyNode(x);//开辟空间
    if(pos==pphead)
    {
        SLTPushFront(pphead,x);//调用头插函数
    }
    else
    {
        SLTNode* prev=*pphead;
        while(prev->next!=pos)//这个while循环不能规避头插的情况(头插:最后会出现空指针解引用的情况)
        {
            prev=prev->next;
        }
        prev->next=newnode;
        newnode->next=pos;
    }
}

//最后一个节点的指针指向NULL,所以在最后一个节点的后面插入时,不会出现问题

//删除某个节点之后不要忘了把指向这个节点的指针置为空
free(pos);
pos=NULL;

//删除某个位置pos之后的节点
pos->next=pos->next->next;
free(pos->next);
pos->next=NULL;
//这是错误的,把pos->next释放掉了,就相当于把pos->next->next释放掉了,导致pos为野指针
//正确做法:把初始的pos->next存储下来
//删除倒数第二个位置的下一个即删除最后一个,也可以实现,因为最后一个节点指向NULL
SLTNode* del=pos->next;
pos->next=del->next;
free(del);
del=NULL;

//链表的销毁
void SListDestroy(SLTNode** pphead)//因为要改变头节点的指向
{
    assert(pphead&&*pphead);
    SLTNode* pcur=*pphead;
    while(pcur)
    {
        SLTNode* next=pcur->next;
        free(pcur);
        pcur=next;
    }
    *pphead=NULL;
}

//顺序表不为空有两种情况:(需要特别注意这种情况的是当需要尾删的时候)
//1.传参传的地址是NULL
//2.顺序表有地址即顺序表存在,但是其size为0,当访问下标的时候要size-1,此时就完蛋了

//capacity/size  ++/--

//在指定位置pos之前插入数据
//那么pos>=0&&pos<=size
//最之前可以在以一个数据之前插入即第0个位置
//最之后即可以在最后一个数据的下一个位置之前插入,即实现尾插
//这个断言同时也可以拦截顺序表为空的第二种情况

//对通讯录操作实际上就是对顺序表进行操作,只是把结构体重命名作为顺序表中一个空间的ElemType了
//顺序表头文件要用到通讯录头文件的struct personInfo,而Contact.h要用到SeqList.h的SL,不能互相include
//不能互相包含头文件,交叉使用头文件会导致程序报错
//这里要用到前置声明
//可以写成typedef struct SeqList Contact;//这一步就叫做前置声明
//不可以写成typedef SL Contact;因为在Contact.h文件中没有包含SeqList.h,只能使用最原始的名字;如果包含了就能使用包含的那个头文件中typedef之后的名字了
//在SeqList.h文件中虽然使用了typedef进行了重命名,但是是先定义然后改的名字
//在Contact.c文件中肯定会用到Contact.h中的内容,这个肯定要包含,同时,因为对通讯录操作就是对顺序表进行操作,可以节省一些步骤,所以也要包含SeqList.h文件

//使用o(1)复杂度的程序,删除数组中所有的等于value的值
//也就是说不能再开辟一块空间
//双指针法:创建两个指针变量src、dst同时指向起始位置
//  若src指向的值是value,则src++
//  若src指向的值不是value,则nums[dst]=nums[src],src++,dst++
//  最后src指向结尾,dst移动的次数(dst指向的坐标位置)就是当前数组中还剩多少个数据
int removeElement(int* nums,int numsSize,int val)
{
    int src,dst;
    src=0,dst=0;
    while(src<numSize)
    {
        if(nums[src]==val)
        {
            src++;
        }
        else
        {
            nums[dst]=nums[src];
            src++;
            dst++;
        }
    }
    return dst;//返回新数组的有效长度
}

//将nums1和nums2两个非递减的有序数组合并到nums1这一个数组中
//方法一:
//将nums2中的数据先都放在nums1的后面,然后对nums1进行排序
//方法二:
//三个指针,两个分别指向两个数组的最后一个有效元素,另一个指针指向合并后nums1的最后一个位置
//两个指针处的数据比较大小,谁大谁往第三个指针那里放,然后指针减减,当有指针为0时结束循环
//同时注意:不可能两个指针同时为0,因为一次只挪动一个数据,必定有一个先移完
//结束循环之后,如果nums1中的那个指针不为0不用管,因为nums1本身有序
//如果nums2中的指针不为0,需要将他放到nums1中的第三个指针里
void merge(int* nums1,int m,int* nums2,int n)//m和n分别为nums1和nums2的初始有效个数
{
    int l1=m-1;
    int l2=n-1;
    int l3=m+n-1;
    while(l1>=0&&l2>=0)
    {
        if(nums1[l1]>nums2[l2])
        {
            nums1[l3--]=nums1[l1--];
        }
        else
        {
            nums1[l3--]=nums2[l2--];
        }
    }
    while(l2>=0)
    {
        nums1[l3]=nums2[l2--];
    }
}

//在单链表中插入一个节点时,需要注意如果链表是空链表,那么链表明就是空指针,而空指针不能解引用
//即SLTNode* ptail=phead(被传参的链表);//这样是会报错的
//所以当为空链表的时候,应该让ptail直接指向新的节点
//如果不是空链表就循环找要插入那个点,然后改变指针指向

SLTNode* plist=NULL;
SLTPushBack(plist,1);
void SLTPushBack(SLTNode* phead,SLTDataType x);
//形参的改变要影响实参的话必须要传地址
//这样传参会导致只有在被调用的函数中改变了,除了被调函数的作用域就再恢复原来的状态
//因为要改变的是plist的内容,虽然plist存储的内容是指针,但是只有取地址才能实现地址传参
//才能真正改变plist的内容,即才能实现改变plist指向的空间
//要写成
SLTNode* plist=NULL;
SLTPushBack(&plist,1);//对一级指针取地址就要用二级指针来接收
void SLTPushBack(SLTNode** pphead,SLTDataType x);
//该处pphead不能为NULL,因为后面要对他进行解引用来用指针指向的节点来找位置
//而*pphead可以为空,此处代表pphead的内容为空,即pphead指向NULL
//*plist(解引用) 第一个节点         =**pphead(对*pphead解引用)
//plist 指向第一个节点的指针        =*pphead(对pphead解引用)
//&plist 指向第一个节点的指针的地址 =pphead(形参与实参相等)
//删除节点时,pphead和*pphead都不能为空,因为空指针不能解引用和为空删不了

//链表尾删
void SLTPopBack(SLTNode** pphead)
{
    //链表不为空
    assert(pphead&&*pphead);
    //链表只有一个节点
    if((*pphead)->next==NULL)//->的优先级高于*
    {
        free(*pphead);
        *pphead=NULL;
    }
    //链表有多个节点
    else
    {
        SLTNode* prev=*pphead;
        SLTNode* ptail=*pphead;
        while(ptail->next)
        {
            prev=ptail;
            ptail=ptail->next;
        }
        free(ptail);
        ptail=NULL;
        prev->next=NULL;//当只有一个结点的时候,这一个节点被释放了,就不能再用next来指向下一个了,会非法访问
    }
}

//链表头删
void SLTPopFront(SLTNode** pphead)
{
    assert(pphead&&*pphead);
    //只有一个结点的时候也行,因为那一个结点的下一个是NULL
    SLTNode* next=(*pphead)->next;
    free(*pphead);
    *pphead=next;
}

//当指向链表头部的指针可能发生变化时,传参的链表名就需要使用二级指针

//在指定位置之前插入数据
//先找到那个数据,即找到指向某个节点的指针
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{
    assert(pphead&&*pphead);
    assert(pos);
    //空指针不可以解引用
    //当链表为空时,对应的pos也为空,那么就找不到到底在哪个位置之前插入数据
    SLTNode* newnode=SLTBuyNode(x);//开辟空间
    if(pos==pphead)
    {
        SLTPushFront(pphead,x);//调用头插函数
    }
    else
    {
        SLTNode* prev=*pphead;
        while(prev->next!=pos)//这个while循环不能规避头插的情况(头插:最后会出现空指针解引用的情况)
        {
            prev=prev->next;
        }
        prev->next=newnode;
        newnode->next=pos;
    }
}

//最后一个节点的指针指向NULL,所以在最后一个节点的后面插入时,不会出现问题

//删除某个节点之后不要忘了把指向这个节点的指针置为空
free(pos);
pos=NULL;

//删除某个位置pos之后的节点
pos->next=pos->next->next;
free(pos->next);
pos->next=NULL;
//这是错误的,把pos->next释放掉了,就相当于把pos->next->next释放掉了,导致pos为野指针
//正确做法:把初始的pos->next存储下来
//删除倒数第二个位置的下一个即删除最后一个,也可以实现,因为最后一个节点指向NULL
SLTNode* del=pos->next;
pos->next=del->next;
free(del);
del=NULL;

//链表的销毁
void SListDestroy(SLTNode** pphead)//因为要改变头节点的指向
{
    assert(pphead&&*pphead);
    SLTNode* pcur=*pphead;
    while(pcur)
    {
        SLTNode* next=pcur->next;
        free(pcur);
        pcur=next;
    }
    *pphead=NULL;
}

三、双向链表(双向有节点循环链表)

//初始化 传参传一级指针

//尾插 最后两行不能交换顺序

//注意初始指向哨兵位的下一个位置和while的循环条件

//头插  两行代码顺序不能变

//

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值