数据结构学习——单链表

数据结构学习记录DAY3:单向链表

单链表的基本认识

首先需要介绍一下链表的基本概念:

链表是一种内存可不连续,用指针表示元素之间逻辑关系的存储方式
链表节点除了要存储元素之外,还需要额外的内存地址来存储指针(指针域)

  • 单向链表的特点
    • 只能通过前面的节点找到后面的节点,反之则不行
    • 单向链表会有一个额外的头节点,只负责记录第一个节点的地址

单链表的创建

声名:以下代码均系多文件编程学习中取出,完整文件不知道怎么传,但是我觉得这些足够理解以及简单的使用了。
首先创建一个链表,包含节点元素(节点元素越多,单链表对内存的利用率就越高)和指针域(用于指向下一个节点的内存地址)的结构体,下方代码展示的Snode就是一个最简单的单链表节点,以及如何用这个节点进行单链表的创建以及元素的首位置插入。

typedef int ElemType;
//单向链表节点
struct Snode{
	ElemType elem;//节点元素
	struct Snode *next;//下一个节点的内存地址
}
//单向链表指针
typedef struct Snode * SL
//宏定义一个用于表示节点大小的变量,在多文件编程中便于记忆
#define SNODESIZE sizeof(struct Snode)
//创建一个节点
SL create_slink(void)
{
    SL list = (SL)malloc(SNODESIZE);
    if(NULL != list)
    {
        list->next = NULL;
    }
    return list;
}

//静态函数表示仅在本文件中使用
static struct Snode* create_snode(ElemType elem, struct Snode *next)
{
    struct Snode *node = (struct Snode*)malloc(SNODESIZE);//malloc在<stdlib.h>头文件中
    
    if(NULL != node)
    {
        node->elem = elem;
        node->next = next;
    }
    return node;
}
static int insert_after(struct Snode *node, Elemtype elem)
{
	//传入空指针则创建失败
	if(NULL == node)
	{
		return -1;
	}
	struct Snode *insnode = create_snode(elem, node->next);
	//内存分配成不成功创建失败
	if(NULL != insnode)
	{
		retuen -1;
	}
	node->next = insnode;
	return 0;
}
//指定位置插入 时间复杂度为O(n)
int insert_slink(SL list, size_t pos, ElemType elem)
{
    assert(NULL != list);//断言,包含于头文件<assert.h>中
    if(pos == 0) return FAILURE;

    //取pos前驱,即pos-1的节点
    struct Snode *prev = get_node(list,pos-1);
    return insert_after(prev, elem);
    //不用以上两个静态函数可以直接用注释内的语句实现
    /*
    if(NULL == prev)//插入位置的前节点为空则插入失败
    {
        return FAILURE;
    }
    struct Snode *insnode = create_snode(elem, prev->next);
    if(NULL == insnode)
    {
        return FAILURE;
    }
    prev->next = insnode;

    return SUCCESS;
    */
}
//首个添加 时间复杂度为O(1)
int push_front_slink(SL list, ElemType elem)
{
    assert(NULL != list);
    return insert_after(list, elem);
    /*
    struct Snode *fir = create_snode(elem, list->next);
    if(NULL == fir)
    {
        return FAILURE;
    }
    list->next = fir;
    return SUCCESS;
    */
}

当然单链表还有很多其他的基础操作:一一代码展示实在有些长,就先不做展示了。以下的重要操作才是关键,听老师说链表反转以及链表以n为长度进行反转是面试里会有的题目,建议反复理解。在此也小做复习。

单链表的重要操作

//反转链表
//请配合上面的创建使用
void reverse_slink(SL list)
{
	assert(NULL != list);
	//当链表只有头节点或只有一个节点的时候反转链表没有意义 
	if(list->next == NULL || list->next->nrxt)
	{
		return;
	}
	/*
		背下来代码是一回事,理解起来却又是另一回事。
		链表反转其实就是对于除头节点外的每一个节点来说,使其原本指向
		下一个节点的指针指向其上一个节点的地址 
		但是由于单链表只有一个指向下一个节点的指针,但是没有指向上一个
		节点位置的指针,因此需要一个保存上一个节点位置的指针和一个指向
		下一个节点位置的指针确保在修改节点指针后,下一个节点的位置不至于
		丢失 
	*/
	struct Snode *prev = NULL;//用于存放上一个节点的位置 
	struct Snode *curr = list->next;//用于存放当前节点 
	struct Snode *next = NULL;//用于存放下一个节点 
	while(curr != NULL)
	{
		next = curr->next;
		curr->next = prev;
		prev = curr;
		curr = next;
	 } 
	list->next = prev;
 } 
//以N为长度反转链表
//如原本的1,2,3,4,5,6以3为长度进行反转后就成了:
//3,2,1,6,5,4,听起来挺简单的,单实现起来却不容易
void reverseByCount(SLinkedList list,size_t count)
{
    assert(NULL != list && n != 0);
    if(list->next == NULL || list->next->next == NULL)
    {
        return;
    }
    struct Snode *last = list;
    struct Snode *prev = NULL;
    struct Snode *curr = list->next;
    struct Snode *next = NULL;
    struct Snode *first = NULL;
    while (curr != NULL)
    {
        first = curr;
        prev = NULL;
        for(int i = 0; i < n  && curr != NULL; ++i)
        {
            next = curr->next;
            curr->next = prev;
            prev = curr;
            curr = next;
        }
        last->next = prev; //已经逆序的最后一个结点的next指向 刚刚逆序的count个结点的最后一个 
		last = first;//现在原则这count个的第一个结点变成逆序好的最后一个 

    }
    //return list;
}

//合并两个升序的单向链表为一个  l1和l2都是升序的,合并到l3中保持有序
//思路介绍:分别用两个指针指向两链表的数据域,比较两指针的数据大小,较小者添加到新链表中,同时指针指向后一个节点
void merger_slink(SL list1, SL list2, SL s3)
{
    assert(NULL != list1 && NULL != list2 && NULL != s3);
    struct Snode *node1 = list1->next;
    struct Snode *node2 = list2->next;
    struct Snode *curr = s3;
    while (node1 != NULL && node2 != NULL)
    {
        if(node1->elem < node2->elem)
        {
            curr->next = node1;
            curr = node1;
            node1 = node1->next;
        }
        else
        {
            curr->next = node2;
            curr = node2;
            node2 = node2->next;
        }
        curr->next = node1 != NULL ? node1 : node2;
    }
    list1->next = NULL;
    list2->next = NULL;
    //return ;
}


//判断一个单向链表是否有环
//思路介绍:用一个快指针和一个慢指针遍历链表,若快指针“追上”慢指针,则有环
int circle_slink(SL list)
{
    assert(NULL != list);
    if(list->next == NULL || list->next->next == NULL)
    {
        return 0;
    }
    struct Snode *quick = list->next;
    struct Snode *slow = list->next;

    while(quick != NULL && quick->next->next != NULL)
    {
        quick = quick->next->next;
        slow = slow->next;
        if(quick == slow)
        {
            size_t n = 0;
            do{
                ++n;
                quick = quick->next;
            }while(quick != slow);
            return n;
        }
    }
}


//求两单向链表共同尾串的长度
/*思路介绍:当两个链表有共同子串的时候,该子串的长度最多为较短的链表的长度(len2),因此,可以从较长子串(len1)的后len1-len2个节点的位置开始比较
*/
size_t common_tail(SL l1, SL l2)
{
     assert(NULL != l1 && NULL != l2);
     size_t len1 = size_slink(l1);
     size_t len2 = size_slink(l2);
     struct Snode *node1 = l1;
     struct Snode *node2 = l2;
     if(len1 > len2)
     {
        while(len1 != len2)
        {
            --len1;
            node1 = node1->next;
        }
     }
     else
     {
         while(len1 != len2)
        {
            --len2;
            node2 = node2->next;
        }
     }
    while(node1 != NULL && node2 != NULL && node1 != node2)
    {
        --len1;
        node1 = node1->next;
        node2 = node2->next;      
    }
    return len1;
}
  • 单向链表的优缺点:

    • 优点:
      • 内存不需要连续,可以是零散的内存空间
      • 在头部插入和删除的效率非常高 O(1)
      • 在指定节点的后面插入元素或删的效率非常高
      • 存储元素不需要提前分配内存,有多少元素分配相应的内存空间,不会有内存闲置
    • 缺点
      • 有指针域,内存利用率不高
      • 不支持随机访问,查找指定位置的元素的时间复杂度为O(n)
      • 在末尾插入和删除的效率低
      • 不能对给定的节点进行删除操作,也不能在给定节点之前插入
      • 即使元素有序也不能进行二分查找
  • 单向链表的使用时机

    • 数据量未知

    • 数据变化较多

    • 增加和删除常在头部进行时

      比较项目顺序表单链表
      插入在末尾插入时间复杂度为O(1)
      插入的平均时间复杂度为O(n)
      给定位置,在其后面插入的时间复杂度O(n)
      在头部插入时间复杂度为O(1)
      插入的平均时间复杂度为O(n)
      在给定位置(结点)其后插入的时间复杂度为O(1)
      删除同上同上
      查找指定元素平均时间复杂度为O(n)
      如果有序,二分查找,时间复杂度为O(logn)
      平均时间复杂度为O(n)
      如果有序,时间复杂度依然为O(n),无法使用二分查找
      索引时间复杂度为O(1)
      支持随机访问
      时间复杂度为O(n)
      内存连续内存
      预先分内存,配多了内存浪费,少了内存不够,扩容麻烦
      不要求连续
      需要额外内存分配给指针
      需要多少用多少
      使用场合数据量变化不大
      常在末尾插入和删除
      需要经常根据位置操作元素
      数据变化量较大(变化量较小也可使用
      经常在头部进行插入和删除
      没有前提条件,选单向链表
  • 单向循环链表

    • 实际上没有意义,就是单向链表的最后一个结点next指向头结点

以上!

-------------------------------------------------------------------------
希望能给你一点帮助。
感谢

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值