单链表的查找

前言

上一篇文章中,我们详细的讲解了单链表插入和删除的操作——单链表的插入和删除

在这一小节,我们继续探讨单链表的查找。要注意的是,这篇文章的查找都是基于带头结点的链表实现。

按位查找

按位相信大家已经不陌生了,就是根据给定的位序进行操作。

GetElem(L,i):在表L中,找到第i位的结点。

代码实现

其实在上篇文章中的插入和删除操作中,我们已经实现了按位查找,只需要在原来的基础上进行一些修改即可。

LNode * GetElem(LinkList L,int i){
    if(i<0)
        return NULL;
    LNode *p;			// 指针p指向当前扫描到的结点
    int j = 0;			// 当前p指向的是第几个结点
    p = L;				// 将p指向头结点L(注意:头结点是第0个元素,并不是链表中的真正结点)
    while(p!=NULL && j<i){	// 循环找到第i个结点
        p = p->next;
        j++;
    }
    
    return p;			// 返回找到的结点
}

由于注释已经将代码的各部分功能讲述的比较清晰,故不再进行大篇幅的介绍代码功能。

我们可以看到其实和上一篇文章中插入和删除的查找是一样的,唯一区别就是j<i。我们在插入和删除操作是需要找到要删除的结点的前继,所以需要匹配到i-1的位置,而在按位查找中,我们是需要精确定位到我们要查找的结点,故修改j<i-1j<i

我们书写代码的时候需要考虑一些情况,以此来检测代码的健壮性:(我们假设链表的长度为4)

  • 当i=0时
    • 那么代码会返回头结点的地址
  • 当i=8(超过链表的长度时)
    • 那么代码会返回NULL
  • 当i=2(在链表的长度范围内)
    • 那么代码会返回第2个结点的地址(需要查找的结点的地址)

时间复杂度(平均)

从上面的分析,显然我们可以得出平均时间复杂度为O(n)

封装(基本操作)的好处

在这里我们单独的将查找的操作进行了实现,所以我们在插入和删除操作中,就可以将查找的代码直接替换为调用GetElem(L,i-1),当然由于插入和删除操作是要找到前继结点,故传入i-1

image-20220708131421575

到此,我们又体会到了封装的好处,也就是将需要重复实现的功能封装成函数,当我们需要使用某些功能时,只需要调用对应的函数即可,这大大的降低了代码的冗余性,解放了我们的双手。避免了重复代码也就实现了:简洁和易维护。简洁大家应该都体会到了,但是易维护是什么意思呢?

还是以上面的例子来说明。如果查找的代码你不是使用封装,而是分别在需要查找功能的地方写上查找的代码。当有一天你发现你的查找代码有些问题想要修改时,你会发现有很多处都需要修改,这工作量无疑是巨大的。但是如果你是使用封装并且在需要的地方调用,你就会发现你只需要修改封装的代码即可。

注重细节

并且我们要注意,在之前的后插操作的函数中会有一个传入结点是否为NULL的判断

if(p==NULL)
    return false;

可能会有小伙伴有疑问,真的会有人会故意传入NULL吗?其实从封装性就可以很好的回答这个问题。

当我们将查找和后插分别封装成函数后,我们会发现按位插入不再需要像上篇文章中一样书写很多代码,只需要两行代码即可

LNode *p = GetElem(L,i-1);		// 找到第i-1个结点
return InsertNextNode(p,e);		// 进行后插操作

image-20220708132348453

InsertNextNode()

bool InsertNextNode(LNode *p,ElemType e){
    if(P==NULL)
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s==NULL)				// 内存分配失败
        return false;
    s->data = e;			// 将e存入新结点
    s->next = p->next;
    p->next = s;			// 将结点s连接到p之后
    
    return true;
}

GetElem()

LNode * GetElem(LinkList L,int i){
    if(i<0)
        return NULL;
    LNode *p;			// 指针p指向当前扫描到的结点
    int j = 0;			// 当前p指向的是第几个结点
    p = L;				// 将p指向头结点L(注意:头结点是第0个元素,并不是链表中的真正结点)
    while(p!=NULL && j<i){	// 循环找到第i个结点
        p = p->next;
        j++;
    }
    
    return p;			// 返回找到的结点
}

在查找结点的函数中,当为指针指向头结点或者要查找的范围超过链表长度时,都会返回NULL。所以在InsertNextNode()中判断p结点是否存在就显得很有必要,无疑是增强了代码的健壮性。

按值查找

按值的含义就是根据所给的值在链表中匹配,看看是否又符合该值的元素。

代码实现

LNode * LocateElem(LinkList L,ElemType e){
    LNode *p = L->next;				// 让p指针指向头结点的下一个结点(链表的第一个存放元素的结点)
    while(p!=NULL && p->data != e)	// 循环查找
        p = p->next;
    
    return p;						// 返回找到的结点的地址(指针)
}

代码分析

  1. 按值查找我们,一般就是从第一个结点开始查找
    • 因为按值查找是需要查找数据域内存放数据的第一个结点,而头结点不存放任何数据,故从第一个结点开始查找
  2. while循环的条件:当p指向结点(不为NULL)并且p指针指向的结点的数据域和需要查找的值不匹配,则进行循环
    • 循环内部是将p指针指向下一个结点
  3. 返回找到的结点的地址(指针),若没找到则返回NULL
    • 没找到对应的结点,说明已经遍历完整个链表,那么遍历完整个链表,最终p结点会指向NULL

注意:如果链表中存放的是结构体的类型,那么我们需要对比结构体中的分量值的相同与否来判断是否是我们要查找的元素,而不是直接p->data != e

在之前的文章中我们也提到过如何比较结构体类型的数据

求表的长度

int Length(LinkList L){
    int len = 0;
    LNode *p = L;
    while(p->next!=NULL){	// 遍历链表
        p = p->next;
        len++;
    }
    
    return len;
}

求表长的代码逻辑很简单,就是通过遍历各个结点,循环1次,len长度就+1。

时间复杂度

由于求表长需要遍历整个链表,所以时间复杂度为O(n)

结束语

已同步更新至个人博客:https://www.hibugs.net/index.php/linklistsearch/

本人菜鸟一枚,仅分享学习笔记和经验。若有错误欢迎指出!共同学习、共同进步 😃

如果您觉得我的文章对您有所帮助,希望可以点个赞和关注,支持一下!十分感谢~(若您不想也没关系,只要文章能够对您有所帮助就是我最大的动力!)

下一篇文章传送门:正在更新,敬请期待…

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值