线性表—不带头单向非循环链表的增删查改

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线性表—不带头单向非循环链表的增删查改相关的知识,希望对你有一定的参考价值。

@​​TOC​

前言

回顾之前的顺序表,我们发现就算是动态扩容,我们也都是成倍的括,也可能存在空间浪费,并且顺序表的头插头删还十分麻烦,需要挪动数据。

而链表的存在就解决了头插头删以及空间浪费这一问题,提到链表,我们脑海中就会浮现出一个链条把东西都链接起来。

【线性表】—不带头单向非循环链表的增删查改_野指针

链表

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。这里所谓的逻辑结构,其实就是为了方便理解,然后加上箭头用来表示关系的,但实际上并不存在箭头。

【线性表】—不带头单向非循环链表的增删查改_链表_02

我们发现,链式结构其实就是在该节点存放下一个节点的地址,然后通过地址便可以访问到该节点的下一个节点。而上图中的箭头,只是为了方便理解,一个一个连接起来,但实际上是并不存在的。(逻辑结构)

因此,链式结构在逻辑上是连续的(如上图通过箭头链接起来),但在物理地址上却不一定连续。因为每一个节点都是在堆上开辟空间,开辟空间的地址有可能连续,又可能不连续。

链表种类

链表主要分为以下几类:单向与双向、带头与不带头、循环与非循环,而通过这三类的组合,又分为八种形式的链表:带头单向循环链表、带头单向不循环......

【线性表】—不带头单向非循环链表的增删查改_数据_03


【线性表】—不带头单向非循环链表的增删查改_野指针_04


【线性表】—不带头单向非循环链表的增删查改_链表_05

而我们本次章节研究的就是不带头单向非循环链表。把这一个连接后,后面的其它种类的链表就很好理解与实现了

【线性表】—不带头单向非循环链表的增删查改_数据_06

接口实现

typedef int SLTDateType;
typedef struct SListNode

SLTDateType data;//数据
struct SListNode* next;//指向下一个结构体的指针
SListNode;

动态申请节点

动态申请节点其实就是在堆上malloc出一块空间,并把数据data存放在该节点中。由于后面的插入操作都需要进行开辟新空间,所以这里单独给写了出来,后面用到的时候直接调用即可

//动态申请节点
SListNode* BuySListNode(SLTDateType x)

SListNode*newnode=(SListNode*)malloc(sizeof(SListNode));//malloc空间
if (newnode == NULL)

perror("malloc fail");
exit(-1);

newnode->data = x;//将数据放在该节点的data
newnode->next = NULL;//next置为空(避免野指针)
return newnode;//返回新节点

尾插与尾删

尾插

还是需要进行画图,这样才能更好的理解

【线性表】—不带头单向非循环链表的增删查改_野指针_07

但是这里假如传来的是个空指针,即假如是一个空的链表,那尾插时这个新节点就作为头节点来使用。(特殊情况)

//尾插
void SListPushBack(SListNode** pplist, SLTDateType x)

//动态申请一个新节点
SListNode* newnode = BuySListNode(x);
//空表
if (*pplist == NULL)

//这里改变List,传址调用、形参是实参的临时拷贝,形参的修改不会影响实参,所以传List的地址,即用二级指针变量来接收一级指针的地址,这时解引用后就会影响到List
*pplist = newnode;

else
//找尾巴
SListNode* ptail = *pplist;
//这里修改的是结构体成员变量,所以不用二级指针
while (ptail->next != NULL)

ptail = ptail->next;

//将节点接在尾巴上
ptail->next = newnode;

这里需要注意的是,我是在外面定义的是一个结构体指针,要对此进行修改,必须传址调用,因为传值调用形参的改变不会影响实参。而进行修改后(空表情况下进行尾插),后面的再次尾插其实改变的就不是该变量了,而是该变量的结构体成员next,以及next节点指向的data

【线性表】—不带头单向非循环链表的增删查改_链表_08


【线性表】—不带头单向非循环链表的增删查改_野指针_09

尾删

画图解决一切。

【线性表】—不带头单向非循环链表的增删查改_数据_10

这里需要注意的就是,假如只有一个节点的情况下,该节点的next就是空指针,然后再next就形成了空指针的解引用操作(NULL->next)这是错误的,所以我们要考虑到只剩一个节点的特殊情况,另外,还要注意空表状态是不可删除的。

//尾删
void SListPopBack(SListNode** pplist)

//空表不可进行删除,所以加个断言
assert(*pplist);
//只有一个数据时直接把该节点释放,然后置空即可
if ((*pplist)->next == NULL)

free(*pplist);
*pplist = NULL;

else

SListNode* ptail = *pplist;//本意即SListNode*ptail=list
//找到最后一个的前一个
while (ptail->next->next!=NULL)

ptail = ptail->next;

//释放最后一个节点
free(ptail->next);
//并把指向最后一个节点的next置空(不置空就是野指针了,因为虽然释放了那块空间,但是它的前一个节点的next依然指向它)
ptail->next = NULL;

打印

这里我们写一个打印的接口,方便我们观察。也很简单,遍历整个链表即可。

//单链表打印
void SListPrint(SListNode* phead)

SListNode* cur = phead;
//注意这里是cur!=NULL,因为假如是cur->next !=NULL的话,最后一个节点的数据不会被打印
while (cur != NULL)

printf("%d->", cur->data);
cur = cur->next;

printf("NULL");

这里我们来测试一下前面的尾插与尾删操作。

void SListTest2()

SListNode* list = NULL;
//传址调用,解引用后形参的修改会影响实参
SListPushBack(&list, 0);
//SListPrint(list);//0->NULL
SListPushBack(&list, 1);
SListPushBack(&list, 2);
SListPushBack(&list, 3);
SListPushBack(&list, 4);
//打印
//SListPrint(list);//0->1->2->3->4->NULL
//尾删
SListPopBack(&list);
//SListPrint(list);//0->1->2->3->NULL
SListPopBack(&list);
SListPopBack(&list);
SListPopBack(&list);
SListPopBack(&list);
SListPrint(list);//NULL
//空表进行删除
SListPopBack(&list);//报错 error

通过测试我们发现一切都在掌控之中,并没有什么问题。接下来实现头插与头删。

头插与头删

头插

单链表的头插最为简单,时间复杂度达到了O(1),还是通过画图从而更好的理解。这里只需要将新节点的next指向目前的头指针,然后头指针再更新为新节点即可。

【线性表】—不带头单向非循环链表的增删查改_野指针_11

//头插
void SListPushFront(SListNode** pplist, SLTDateType x)

//注意这里形参是二级指针,因为这里改变的是list,并不是改变list指向的结构体成员,所以传地址,而一级指针的地址,就要用二级指针pplist接收
SListNode* newnode = BuySListNode(x);
newnode->next =*pplist;
*pplist = newnode;

我们假定是个空链表,我们发现头插也是适用的。因此就以上代码就够了。 头删

【线性表】—不带头单向非循环链表的增删查改_数据_12

这里我们需要注意的就是,空表不可进行删除,然后其余的画个图就一目了然,需要注意的是,这里依然是改变的list,所以还是用二级指针。

//头删
void SListPopFront(SListNode** pplist)

assert(*pplist);
//先保存
SListNode* next = (*pplist)->next;
free(*pplist);
*pplist = next;

测试

//头插头删
void SListTest3()

//头插
SListNode* list = NULL;
SListPushFront(&list,1);
//SListPrint(list);//1->NULL
SListPushFront(&list, 2);
SListPushFront(&list, 3);
SListPushFront(&list, 4);
//SListPrint(list);//4->3->2->1->NULL
//头删
SListPopFront(&list);
//SListPrint(list);//3->2->1->NULL
SListPopFront(&list);
SListPopFront(&list);
SListPopFront(&list);
SListPrint(list);//NULL
//SListPopFront(&list);//error(空表不可删除)

这里我们通过测试,进行观察发现没有什么问题,接下来是单链表的查找。

查找

查找操作也很简单,无非就是遍历整个链表,然后找到data时返回该节点指针即可,找不到就返回空指针。其实也可以这样来实现:

//单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)

SListNode* cur = plist;
//while (cur)
//
// if (cur->data == x)
//
// return cur;
//
// cur = cur->next;
//
//return NULL;
//简化版本
while (cur && cur->data != x)

cur = cur->next;

//结束循环的条件,要么就是cur== NULL,说明找不到,或者就是cur->data==x,找到了,这里直接返回cur就行。
return cur;

任意位置插入与删除

pos位置进行插入

思路都在图纸当中,画图会更加容易理解!

【线性表】—不带头单向非循环链表的增删查改_野指针_13

//在pos位置插入
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)

if (*pplist == pos)

//头插
SListPushFront(pplist, x);

else

SListNode* cur = *pplist;
//找到pos节点之前的一个
while (cur->next != pos)

cur = cur->next;

//将新节点插入在此
SListNode* newnode = BuySListNode(x);
//找到了pos位置之前的了
cur->next = newnode;
newnode->next = pos;

pos位置进行删除

【线性表】—不带头单向非循环链表的增删查改_链表_14

//删除pos位置
void SListErase(SListNode** pplist, SListNode* pos)

assert(pos);
//假如pos指向list
if (pos == *pplist)

SListPopFront(pplist);

else

SListNode* cur = *pplist;
//找到pos位置之前的
while (cur->next != pos)

cur = cur->next;

cur->next = pos->next;
free(pos);

销毁

最后便是单链表的销毁,因为我们知道,malloc、realloc、calloc这几个函数都是与free成对出现的。不然会造成内存泄漏。这里我们也是需要进行遍历每一个节点,然后进行删除,不过需要注意的是在删除该节点之前。要先记住下一个节点。

【线性表】—不带头单向非循环链表的增删查改_链表_15

//单链表销毁
void SListDestroy(SListNode** pplist)

SListNode* cur = *pplist;
while(cur != NULL)

SListNode* next = cur->next;
free(cur);
cur = next;

*pplist = NULL;//一定要置空,不然后面打印就是野指针的访问

总结

在这里,一定要多画图,根据图形来理清思路,然后再进行写代码,同时一定要考虑考虑特殊情况,比如空表状态下能不能删除,比如free的时候会不会存在野指针, 并且还建议大家边写边调试,不要一口气从尾插写完,要逐步进行,慢即是快!


end生活原本沉闷,但跑起来就会有风!❤

java数据结构-快速了解链表的增删查改

一、链表的概念和结构

1.链表的概念

简单来说链表是物理上不一定连续,但是逻辑上一定连续的一种数据结构

2.链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构. 单向和双向,带头和不带头,循环和非循环。排列组合和会有8种。
但我这只是实现两种比较难的链表,理解之后其它6种就比较简单了
1.单向不带头非循环链表
2.双向不带头非循环链表

二、单向不带头非循环链表

在这里插入图片描述

1.创建节点类型

我们创建了一个 ListNode 类为节点类型,里面有两个成员变量,val用来存储数值,next来存储下一个节点的地址。
还有一个带一个参数的构造方法在实例化对象的同时给val赋值,因为我们不知道下一个节点的地址所以next是默认值一个null

class ListNode {
    public int val;//数值
    public ListNode next;//下一个节点的地址

    public ListNode(int val) {
        this.val = val;
    }
}

我们在 MyLinkedList 里创建一个head变量来标识链表的头部,接着就是实现单链表的增删查改了

在这里插入图片描述

2.头插法

这个头插法并不要考虑第一次插入,每次插入只需要把插入的节点node 的next值改成头节点,再把头节点指向node
在这里插入图片描述

//头插法
public void addFirst(int data) {
    ListNode node = new ListNode(data);
    node.next = this.head;
    this.head = node;
}

3.尾插法

尾插法首先要考虑是不是第一次插入,如果是的话直接把head指向node就好了,如果不是第一次插入,则需要定义一个cur来找尾巴节点,把尾巴节点的next值改成node就好了。因为如果不用尾巴节点的话,head就无法标识到头部了

在这里插入图片描述

//尾插法
public void addLast(int data) {
    ListNode node = new ListNode(data);
    ListNode cur = this.head;
    //第一次插入
    if(this.head == null) {
        this.head = node;
    }else{
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = node;
    }
}

4.获取链表长度

定义一个计数器count,当cur遍历完链表的时候直接返回count就好

//得到单链表的长度
public int size() {
    int count = 0;
    ListNode cur = this.head;
    while (cur != null) {
        cur = cur.next;
        count++;
    }
    return count;
}

5.任意位置插入

我们假设链表的头是从0位置开始的,任意位置插入需要考虑几点
1.位置的合法性,如果位置小于0,或者大于链表长度则位置不合法
2.如果要插入的是0位置直接使用头插法
3.如果插入的位置等于链表长度则使用尾插法,因为我们这链表是从0开始的

4.最关键的就是从中间任意位置插入 要从中间位置插入,就需要找到要插入位置的前一个节点的位置。再插入到它们中间。
在这里插入图片描述

    /**
     * 让 cur 向后走 index - 1 步
     * @param index
     * @return
     */
public ListNode findIndexSubOne(int index) {
    int count = 0;
    ListNode cur = this.head;
    while (count != index-1) {
        cur = cur.next;
        count++;
    }
    return  cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data) {
    //判断合法性
    if(index < 0 || index > size()) {
            System.out.println("index位置不合法");
            return;
    }
    //头插法
    if(index == 0) {
        this.addFirst(data);
        return;
    }
    //尾插法
    if(index == size()) {
        this.addLast(data);
        return;
    }
    //找前驱,cur指向的是 index 的前一个节点
    ListNode cur = findIndexSubOne(index);
    ListNode node = new ListNode(data);
    node.next = cur.next;
    cur.next = node;
}

6.查找关键字

当我们要查找链表中是否有某一个关键字时,只需要定义一个cur从头开始遍历即可

//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
    ListNode cur = this.head;
    while (cur != null) {
        if(cur.val == key) {
            return true;
        }
        cur = cur.next;
    }
    return false;
}

7.删除第一次出现值为key的节点

这个思路其实也很简单,考虑到两种情况即可

1.如果要删除的是头节点只需要把头节点指向它的向一个节点即可
2.还有一种则是不存在key的情况,所以这里写了一个方法来判读key是否存在,如果存在则返回key的前一个节点的位置
3.存在则把要删除的节点的前驱的next改成它的next即可

/**
  * 找要删除 key 的前一个节点
 * @return
 */
public ListNode searchPrev(int key) {
    ListNode prev = this.head;
    while (prev.next != null) {
        if (prev.next.val == key) {
            return prev;
        }
        prev = prev.next;
    }
    return null;
}
//删除第一次出现关键字为key的节点
public void remove(int key) {
    if(this.head.val == key) {
        this.head = this.head.next;
        return;
    }
    //找 key 的前驱节点
    ListNode prev = searchPrev(key);
    if(prev == null) {
        System.out.println("没有key这个关键字");
        return;
    }
    //删除
    ListNode delete = prev.next;
    prev.next = delete.next;
}

8.删除所有值为key的节点

在这里插入图片描述

假设要删除的是3,思路:

定义两个节点点类型的变量,prev指向head,cur指向head的下一个节点。
如果判断cur的val值是要删除的值,如果是则直接跳过这个节点 如果不是则让prev和cur往后走,直到整个链表遍历完。
到最后会发现头节点并没有遍历到,循环结束后则需要判读头节点是不是要删除的节点

记住一定要边画图边写代码!

//删除所有值为key的节点
public void removeAllKey(int key) {
    ListNode prev = this.head;
    ListNode cur = this.head.next;
    while (cur != null) {
        if(cur.val == key) {
            prev.next = cur.next;
            cur = cur.next;
        }else {
            prev = cur;
            cur = cur.next;
        }
    }
    //判断第一个节点是否是要删除的节点
    if(this.head.val == key) {
        this.head = this.head.next;
    }
}

9.遍历打印链表

定义一个cur直接遍历打印就好

//打印链表
public void display() {
    ListNode cur = this.head;
    while (cur != null) {
        System.out.print(cur.val+" ");
        cur = cur.next;
    }
    System.out.println();
}

10.置空链表

置空链表只需要一个个置空即可,并不建议直接把头节点置空这种暴力做法

//置空链表
public void clear() {
    ListNode cur = this.head;
    //一个个制空
    while (cur != null) {
        ListNode curNext = cur.next;
        cur.next = null;
        cur = curNext;
    }
    this.head = null;
}

二、双向不带头非循环链表

双向链表和单向链表的最大的区别就是多了一个前驱节点prev,同样来实现双向链表的增删查改

在这里插入图片描述

public class TestLinkedList {
    public ListNode head;
    public ListNode last;
}

1.创建节点类型

同样先定义节点类型,比单向链表多了一个前驱节点而已。

class ListNode {
    public int val;
    public ListNode prev;
    public ListNode next;

    public ListNode (int val) {
        this.val = val;
    }
}

双向链表还定义了一个last来标识尾巴节点,而单链表只是标识了头节点。

在这里插入图片描述

2.头插法

因为这是双向链表,第一次插入要让head和last同时指向第一个节点。
如果不是第一次插入,则需要
1.把head的前驱节点改成node,
2.再把node的next改成head,
3.然后把头节点head再指向新的头节点node。

在这里插入图片描述

//头插法
public void addFirst(int data) {
    ListNode node = new ListNode(data);
    //第一次插入
    if(this.head == null) {
        this.head = node;
        this.last = node;
    }else {
        head.prev = node;
        node.next = this.head;
        this.head = node;
    }
}

3.尾插法

双向链表有一个last来标识尾巴节点,所以在尾插的时候不用再找尾巴节点了。和头插法类似

//尾插法
public void addLast(int data) {
    ListNode node = new ListNode(data);
    //第一次插入
    if(this.head == null) {
        this.head = node;
        this.last = node;
    }else {
        this.last.next = node;
        node.prev = this.last;
        this.last = node;
    }
}

4.获取链表长度

这个和单链表一样,直接定义个cur遍历

//得到链表的长度
public int size() {
    ListNode cur = this.head;
    int count = 0;
    while (cur != null) {
        count++;
        cur = cur.next;
    }
    return count;
}

5.任意位置插入

任意位置插入也和单链表类似有三种情况。判断合法性和头插尾插就不多了主要还是在中间的随机插入,一定要注意修改的顺序!
要修改的地方一共有四个,一定要画图理解!
在这里插入图片描述

//找要插入的节点的位置
public ListNode searchIndex(int index) {
    ListNode cur = this.head;
    while (index != 0) {
        cur = cur.next;
        index--;
    }
    return  cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data) {
    //判断index位置的合法性
    if(index < 0 || index > this.size()) {
        System.out.println("index的位置不合法");
        return;
    }
    //头插法
    if(index == 0) {
        this.addFirst(data);
        return;
    }
    //尾插法
    if(index == this.size()) {
        this.addLast(data);
        return;
    }
    //中间插入
    ListNode node = new ListNode(data);
    ListNode cur = searchIndex(index);
    node.next = cur;
    node.prev = cur.prev;
    cur.prev.next = node;
    cur.prev = node;
}

6.查找关键字

这里和单链表一样,直接定义个cur遍历看看链表里有没有这个值即可

//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
    ListNode cur = this.head;
    while (cur != null) {
        if(cur.val == key) {
            return true;
        }
        cur = cur.next;
    }
    return false;
}

7.删除第一次出现的关键字key的节点

思路:遍历链表找第一次出现的节点,删完return。一共分三种情况
1.头节点是要删除的节点
2.尾巴节点是要删除的节点
3.中间的节点是要删除的节点

//删除第一次出现关键字为key的节点
public void remove(int key) {
    ListNode cur = this.head;
    while (cur != null) {
        if(cur.val == key) {
            //要删除的是头节点
            if(this.head == cur) {
                this.head = this.head.next;
                this.head.prev = null;
            }else {
                //尾巴节点和中间的节点两种情况
                cur.prev.next = cur.next;
                if(this.last == cur) {
                    //删除尾巴节点
                    cur = cur.prev;
                }else {
                    cur.next.prev = cur.prev;
                }
            }
            //已经删完了
            return;
        }else {
            cur = cur.next;
        }
    }
}

8.删除所有值为key的节点

思路和删除一个key类似,但需要注意两个点。

1.删完就不用return了,而是继续往后走,因为这里是删除所有为key需要把列表遍历完
2.还有就是要考虑当整个链表都是要删除的情况,if判断一下不然会发生空指针异常

//删除所有值为key的节点
public void removeAllKey(int key) {
    ListNode cur = this.head;
    while (cur != null) {
        if(cur.val == key) {
            //要删除的是头节点
            if(this.head == cur) {
                this.head = this.head.next;
                //假设全部是要删除的节点
                if(this.head != null) {
                    this.head.prev = null;
                }else {
                 //防止最后一个节点不能被回收
                 this.last = null;
                }
            }else {
                //尾巴节点和中间的节点两种情况
                cur.prev.next = cur.next;
                if(this.last == cur) {
                    //删除尾巴节点
                    cur = cur.prev;
                }else {
                    cur.next.prev = cur.prev;
                }
            }
            //走一步
            cur = cur.next;
        }else {
            cur = cur.next;
        }
    }
}

9.遍历打印链表

//打印链表
public void display() {
    ListNode cur = this.head;
    while (cur != null) {
        System.out.print(cur.val+" ");
        cur = cur.next;
    }
    System.out.println();
}

10.置空链表

遍历链表一个一个置为null,再把头节点和尾巴节点值为null。防止内存泄漏

//置空链表
public void clear() {
    ListNode cur = this.head;
    //一个一个置空
    while (cur != null) {
        ListNode curNext = cur.next;
        cur.prev = null;
        cur.next = null;
        cur = curNext;
    }
    this.head = null;
    this.last = null;
}

总结

1.这里实现了两种较难的链表:单向不带头非循环和双向不带头非循环
2.链表物理上不一定连续,但逻辑上一定连续。
3.增:链表插入一个元素只需要修改指向,所以时间复杂度为O(1)
4:删:链表删除元素,同样只需修改指向,时间复杂度为O(1)
5.查:链表如果需要查找一个元素需要遍历链表,所以时间复杂度为O(n)
6.改:链表要去找到要修改的元素,所以时间复杂度为O(n).
什么时候用链表?
如果是插入删除比较频繁的时候,使用链表。注意:是不涉及到移动数据的情况!

以上是关于线性表—不带头单向非循环链表的增删查改的主要内容,如果未能解决你的问题,请参考以下文章

数据结构入门带头双向循环链表(List)详解(初始化增删查改)

java数据结构-快速了解链表的增删查改

C语言一篇文章带你彻底了解单向链表的增删查改

数据结构学习笔记(单链表单循环链表带头双向循环链表)的增删查改排序等)

数据结构学习笔记(单链表单循环链表带头双向循环链表)的增删查改排序等)

双向带头循环链表的(增删查改)的实现