数据结构学习记录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指向头结点
以上!
-------------------------------------------------------------------------
希望能给你一点帮助。
感谢