链表
单链表
链表(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;
}
};