0
点赞
收藏
分享

微信扫一扫

单链表的基本操作

善解人意的娇娇 2022-04-21 阅读 86
链表

链表

单链表

链表(linked list)是一种物理上非连续,非顺序的数据结构,由若干个(Node)组成

单链表与数组在-增删改查时间复杂度的比较

在这里插入图片描述

数组的优势:

快速定位元素,对于读操作多,写操作少的场景用数组更合适

链表的优势

灵活的插入和删除元素,如果需要在尾部频繁插入,删除元素,用链表更合适一些

单链表的结构

在这里插入图片描述

每个单链表的结点都包含两部分的内容

typedef int ELEMTYPE;
typedef struct Node{
    ELEMTYPE data;//数据域
    struct Node *next;//地址域
}

链表的存储方式

随机存储–数组的内存分配方式,是数组在内存中占用了连续完成的存储空间,链表则是采用了随机存储的方式,链表的每一个结点都分布在内存的不同位置,依靠next指针连接起来,这样可以有效的利用零散的碎片空间。

链表的基本操作

链表的初始化
//单链表的初始化
void Init_List(PNode pn){
    assert(pn!=NULL);//判断是否申请到头节点
    pn->next=NULL;
}
头插建立单链表
//头插法建立单链表
bool Insert_head(PNode pn,ELEMTYPE val){
    //判断头节点是否为空
    assert(pn!=NULL);
    //从堆内存中申请一个新的节点 pnewnode为节点 *pnewnode为新节点的地址
    struct Node *pnewnode=(struct Node *)malloc(sizeof(struct Node));
    //判断是否申请到这个新节点
    //assert函数的内部实现是什么原理不清楚
    assert(pnewnode!=NULL);
    pnewnode->data=val;
    //这里也可以使用-这中方式,因为是头插构建单链表,所以新建立的单链表的next一定为空
    /*pn->next=pnewnode;
    pnewnode->next=NULL;*/
    //从头部插入-头节点next指向新节点的next,新节点的next指向头节点的next;
    pnewnode->next=pn->next;
    pn->next=pnewnode;
    return true;
}
尾插法建立单链表
//尾插法建立单链表
bool Insert_tail(PNode pn,ELEMTYPE val){
    //第一步判断单链表是否为空
    assert(pn!=NULL);
    //第2步在堆内存上购买的一个新的节点
    struct Node *pnewnode=(struct Node *) malloc(sizeof (struct Node));
    //判断是否申请成功
    assert(pnewnode!=NULL);
    //尾部插入,所以现在要找到对应的尾部节点的位置让尾部节点next指向新节点
    //所以在这里就需要循环遍历到单链表的尾部节点
    //同时需要申请一个新的节点来存放器最后一个节点的地址
    struct  Node *p=pn;
    //在这里要注意因为最后一个节点的p->next==NULL的,所以循环结束的条件是到达最后一个结点
    for(p;p->next!=NULL;p=p->next);
   //找到对应的信息,让p->next指向pnewnode让pnewnode指向未NULL;
   p->next=pnewnode;
   pnewnode->next=NULL;
   pnewnode->data=val;

   return true;
}
按位置插入
//按位置插入 pos=0表示代表头插
bool Insert_pos(PNode pn,int pos,ELEMTYPE val){
    //第一步依旧判断链表是否为空
    assert(pn!=NULL);
    //判断插入的位置合不合法--如果小于0肯定不合法,如果大于单链表的长度肯定也不合法
    assert(pos>=0&&pos<= Get_length(pn));
    //插入,我们只需要将新节点的next域指向目前位置节点的next,让后让目前所在节点的next指向新节点
    //申请一个新的节点
    struct Node *pnewnode=(struct Node *) malloc(sizeof (struct Node));
    //我先要找到对应的位置,定义一个节点来找对应的位置
    struct Node *p=pn;
    for(int i=0;i<pos;i++){
        p=p->next;
    }
    //找到位置之后开始进行插入操作
    pnewnode->data=val;
    pnewnode->next=p->next;
    p->next=pnewnode;
    return true;
}
按值查找

链表在查找的过程中,与数组不同,不能够通过下标快速定位,只能从头节点注意查找

//按值查找 
struct Node *search(PNode pn,ELEMTYPE val){
    //遍历整个链表,查找是否由对应的值
    for(struct  Node *p=pn->next;p!=NULL;p=p->next){
        if(p->data==val){
            return p;
        }
    }
    //如果没有找到则返回空
    return NULL;
}
四种删除
头部删除
//头删
bool Del_head(PNode pn){
    //第一步继续判断是否为空
    //assert(pn!=NULL);
    //头珊的思想就是-让头节点的next指向第一个节点的next,释放第一个节点,如果不释放会造成内存泄露
    if(IsEmpty(pn)){
        return false;
    }
    struct Node *p=pn->next;//保存第一个几点
    //将头结点的next保存第一个结点next也就是第一个结点
    pn->next=p->next;
    free(p);//释放P
    //将p的next设置为空,防止野指针
    p=NULL;
    return true;
}
尾部删除
//尾部删除
bool Del_tail(PNode pn){
    if(IsEmpty(pn)){
        return false;
    }
    //让p指向倒数第1个结点
    struct Node *p=pn;
    for(p;p->next->next!=NULL;p=p->next);
    //让q指向倒数第二个结点
    struct Node *q=p->next->next;
    //将q的next设置为null_ptr
    free(q);
    p->next=NULL;
    q=NULL;
    //释放p->next->next;最后一个结点

    return true;
}
按位置删除
//按位置删除
bool Del_pos(PNode pn,int pos){
    //同样判断是否为空
    if(IsEmpty(pn)){
        return false;
    }
    //检查删除的位置是否合法
    assert(pos>=0&&pos< Get_length(pn));
    //找到对应要删除上一个结点
    struct Node *p=pn;
    for(int i=0;i<pos;i++){
        p=p->next;
    }
    //位置删除相当于头部删除
    struct Node *q=p->next;
    p->next=q->next;
    free(q);
    q=NULL;
    return true;
}
按值删除
//按值删除
bool Del_val(PNode pn,ELEMTYPE val){
    if(IsEmpty(pn)){
        return true;
    }
    //看链表当中是否有对应的值,直接调用查找函数
    struct Node *p= search(pn,val);
    //如果没有找到则直接返回false
    if(p==NULL){
        return false;
    }
    //找到了那么找到其上一个结点
    struct Node *q=pn;
    for(q;q->next!=p;q=q->next);
    q->next=p->next;
    free(p);
    p=NULL;
    return true;
}
判断是否为空
//判断是否为空
bool IsEmpty(PNode pn){
    //如果头结点的next为空,则链表为空
    if(pn->next==NULL){
        return true;
    }
    return false;
}
清空
//清空单链表就是销毁,要释放在堆上的内存空间
void Clear(PNode pn){
    Destory1(pn);
}
两种销毁方式
借助头节点的销毁
//销毁1--借助头节点的销毁
void Destory1(PNode pn){
    while(pn->next!=NULL){
        struct Node *p;
        p=pn->next;
        pn->next=p->next;
        free(p);
    }
}
不借助头节点的销毁
//销毁2-不借助头节点的销毁
void Destroy2(PNode pn){
    struct Node *p=pn->next;
    pn->next=NULL;
    struct Node *q=p->next;
    while(p!=NULL){
        //q保存p->next;释放p,将q的值再赋给p
        free(p);
        p=q;
    }
}
获取长度
//获取长度
int Get_length(PNode pn){
    int count=0;
    for(struct Node *p=pn->next;p!=NULL;p=p->next){
        count++;
    }
    return count;
}
打印
void Show(PNode pn){
    for(struct Node *p=pn->next;p!=NULL;p=p->next){
        cout<<p->data<<" ";
    }
    cout<<endl;
}

完整的代码及测试

LinkList.h的头文件中

//
// Created by kexib on 2022/4/20.
//
#include <cstddef>
#include <cassert>
#include <iostream>
using namespace std;
#ifndef LINKLIST_LINKLIST_H
#define LINKLIST_LINKLIST_H
#endif //LINKLIST_LINKLIST_H
typedef int ELEMTYPE;
typedef struct Node{
    ELEMTYPE data;
    struct Node *next;
}Node,*PNode;
//链表的初始化
void Init_List(PNode pn);
//头插法
bool Insert_head(PNode pn,ELEMTYPE val);
//尾插
bool Insert_tail(PNode pn,ELEMTYPE val);
//按位置插入
bool Insert_pos(PNode pn,int pos,ELEMTYPE val);
//头删
bool Del_head(PNode pn);
//尾删除
bool Del_tail(PNode pn);
//按位置删除
bool Del_pos(PNode pn,int pos);
//按值删除
bool Del_val(PNode pn,ELEMTYPE val);
//按值查找
struct Node *sarch(PNode pn,ELEMTYPE val);
//判空
bool IsEmpty(PNode pn);
//获取单链表的长度
int Get_length(PNode pn);
//清空
void Clear(PNode pn);
//销毁1
void Destory1(PNode pn);
//销毁2
void Destroy2(PNode pn);
//打印
void Show(PNode pn);

LinkList.cpp文件当中

//
// Created by kexib on 2022/4/20.
//


//使用于带前驱的操作:插入,删除等等
//for(struct Node*p=plist;p->next!=NULL;p=p->next)
//停留在最后一个节点
//p->next->next;
//适用于不带前驱的操作-打印、查找、获取有效值个数
//for(struct Node *p=plist->next;p!=NULL;p=p->next)
//最后结点的下一个
#include "LinkList.h"
//单链表的初始化
void Init_List(PNode pn){
    assert(pn!=NULL);//判断是否申请到头节点
    pn->next=NULL;
}
//头插法建立单链表
bool Insert_head(PNode pn,ELEMTYPE val){
    //判断头节点是否为空
    assert(pn!=NULL);
    //从堆内存中申请一个新的节点 pnewnode为节点 *pnewnode为新节点的地址
    struct Node *pnewnode=(struct Node *)malloc(sizeof(struct Node));
    //判断是否申请到这个新节点
    //assert函数的内部实现是什么原理不清楚
    assert(pnewnode!=NULL);
    pnewnode->data=val;
    //这里也可以使用-这中方式,因为是头插构建单链表,所以新建立的单链表的next一定为空
    /*pn->next=pnewnode;
    pnewnode->next=NULL;*/
    //从头部插入-头节点next指向新节点的next,新节点的next指向头节点的next;
    pnewnode->next=pn->next;
    pn->next=pnewnode;
    return true;
}
//尾插法建立单链表
bool Insert_tail(PNode pn,ELEMTYPE val){
    //第一步判断单链表是否为空
    assert(pn!=NULL);
    //第2步在堆内存上购买的一个新的节点
    struct Node *pnewnode=(struct Node *) malloc(sizeof (struct Node));
    //判断是否申请成功
    assert(pnewnode!=NULL);
    //尾部插入,所以现在要找到对应的尾部节点的位置让尾部节点next指向新节点
    //所以在这里就需要循环遍历到单链表的尾部节点
    //同时需要申请一个新的节点来存放器最后一个节点的地址
    struct  Node *p=pn;
    //在这里要注意因为最后一个节点的p->next==NULL的,所以循环结束的条件是到达最后一个结点
    for(p;p->next!=NULL;p=p->next);
   //找到对应的信息,让p->next指向pnewnode让pnewnode指向未NULL;
   p->next=pnewnode;
   pnewnode->next=NULL;
   pnewnode->data=val;

   return true;
}
//按位置插入 pos=0表示代表头插
bool Insert_pos(PNode pn,int pos,ELEMTYPE val){
    //第一步依旧判断链表是否为空
    assert(pn!=NULL);
    //判断插入的位置合不合法--如果小于0肯定不合法,如果大于单链表的长度肯定也不合法
    assert(pos>=0&&pos<= Get_length(pn));
    //插入,我们只需要将新节点的next域指向目前位置节点的next,让后让目前所在节点的next指向新节点
    //申请一个新的节点
    struct Node *pnewnode=(struct Node *) malloc(sizeof (struct Node));
    //我先要找到对应的位置,定义一个节点来找对应的位置
    struct Node *p=pn;
    for(int i=0;i<pos;i++){
        p=p->next;
    }
    //找到位置之后开始进行插入操作
    pnewnode->data=val;
    pnewnode->next=p->next;
    p->next=pnewnode;
    return true;
}
//头删
bool Del_head(PNode pn){
    //第一步继续判断是否为空
    //assert(pn!=NULL);
    //头珊的思想就是-让头节点的next指向第一个节点的next,释放第一个节点,如果不释放会造成内存泄露
    if(IsEmpty(pn)){
        return false;
    }
    struct Node *p=pn->next;//保存第一个几点
    //将头结点的next保存第一个结点next也就是第一个结点
    pn->next=p->next;
    free(p);//释放P
    //将p的next设置为空,防止野指针
    p=NULL;
    return true;
}
//尾部删除
bool Del_tail(PNode pn){
    if(IsEmpty(pn)){
        return false;
    }
    //让p指向倒数第1个结点
    struct Node *p=pn;
    for(p;p->next->next!=NULL;p=p->next);
    //让q指向倒数第二个结点
    struct Node *q=p->next->next;
    //将q的next设置为null_ptr
    free(q);
    p->next=NULL;
    q=NULL;
    //释放p->next->next;最后一个结点

    return true;
}
//按位置删除
bool Del_pos(PNode pn,int pos){
    //同样判断是否为空
    if(IsEmpty(pn)){
        return false;
    }
    //检查删除的位置是否合法
    assert(pos>=0&&pos< Get_length(pn));
    //找到对应要删除上一个结点
    struct Node *p=pn;
    for(int i=0;i<pos;i++){
        p=p->next;
    }
    //位置删除相当于头部删除
    struct Node *q=p->next;
    p->next=q->next;
    free(q);
    q=NULL;
    return true;
}
//按值查找
struct Node *search(PNode pn,ELEMTYPE val){
    for(struct  Node *p=pn->next;p!=NULL;p=p->next){
        if(p->data==val){
            return p;
        }
    }
    //如果没有找到则返回空
    return NULL;
}
//按值删除
bool Del_val(PNode pn,ELEMTYPE val){
    if(IsEmpty(pn)){
        return true;
    }
    //看链表当中是否有对应的值,直接调用查找函数
    struct Node *p= search(pn,val);
    //如果没有找到则直接返回false
    if(p==NULL){
        return false;
    }
    //找到了那么找到其上一个结点
    struct Node *q=pn;
    for(q;q->next!=p;q=q->next);
    q->next=p->next;
    free(p);
    p=NULL;
    return true;
}
//判断是否为空
bool IsEmpty(PNode pn){
    //如果头结点的next为空,则链表为空
    if(pn->next==NULL){
        return true;
    }
    return false;
}
//清空单链表就是销毁,要释放在堆上的内存空间
void Clear(PNode pn){
    Destory1(pn);
}
//销毁1--借助头节点的销毁
void Destory1(PNode pn){
    while(pn->next!=NULL){
        struct Node *p;
        p=pn->next;
        pn->next=p->next;
        free(p);
    }
}
//销毁2-不借助头节点的销毁
void Destroy2(PNode pn){
    struct Node *p=pn->next;
    pn->next=NULL;
    struct Node *q=p->next;
    while(p!=NULL){
        //q保存p->next;释放p,将q的值再赋给p
        free(p);
        p=q;
    }
}
//获取长度
int Get_length(PNode pn){
    int count=0;
    for(struct Node *p=pn->next;p!=NULL;p=p->next){
        count++;
    }
    return count;
}
void Show(PNode pn){
    for(struct Node *p=pn->next;p!=NULL;p=p->next){
        cout<<p->data<<" ";
    }
    cout<<endl;
}

main.cpp

#include <iostream>
#include "LinkList.h"
int main() {
    struct Node head;
    //链表初始化
    Init_List(&head);
    //头插20个值
    for(int i=0;i<20;i++){
        Insert_head(&head,i+1);
    }
    //显示对应的值
    Show(&head);
    //尾部插入
    for(int i=21;i<31;i++){
        Insert_tail(&head,i);
    }
    Show(&head);
    //再5的位置插入1000
    Insert_pos(&head,5,1000);
    Show(&head);
    //删除头结点
    Del_head(&head);
    Show(&head);
    //删除尾部节点
    Del_tail(&head);
    Show(&head);
    //按位置删除
    Del_pos(&head,10);
    Show(&head);
    //按值删除
    Del_val(&head,1000);
    Show(&head);
    //按值查找
    cout<<search(&head,29)->data<<endl;
    //按位置查找
    cout<<search(&head,5)->data<<endl;
    //获取长度
    cout<<Get_length(&head)<<endl;
    cout<<"------------------------"<<endl;
    //清空单链表
    Clear(&head);
    Show(&head);
    return 0;
}

1道简单的练习题

剑指 Offer 06. 从尾到头打印链表

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:

输入:head = [1,3,2]
输出:[2,3,1]

限制:

0 <= 链表长度 <= 10000

c语言实现

int* reversePrint(struct ListNode* head, int* returnSize){
    struct ListNode *p=head;
    //先遍历一遍记录链表的长度,无头结点的一个链表
    int count=0;
    while(p!=NULL){
        count++;
        p=p->next;
    }
    *returnSize =count;
    static int arry[10000];
    //在这里不能在使用p了因为p已经指向最后一个为空了,会直接跳过while循环,那在这里可以直接使用head结点
    while(head){
        //循环直到p为空,将List中的值倒序放在数组当汇总
        arry[--count]=head->val;
        head=head->next;
    }
    return arry;

}

C++实现

class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
      //直接遍历将值放到vector当中
      vector<int>arr;
      while(head){
          //使用vector的push_back函数将每个值放按顺序放到数组当中
          arr.push_back(head->val);
          head=head->next;
      }
        //再使用reverse函数进行翻转
      reverse(arr.begin(), arr.end());
      return  arr;
    }
};
举报

相关推荐

0 条评论