2.3 单链表

单链表

定义:链式存储的线性表。

链式:列表元素分散在内存各处,每个元素都包含数据域和指针域,并用指针来指向下一个元素。

分类

根据链表第一个结点是否存储数据,将链表分为带头结点的链表和不带头结点的链表。

带头结点链表

链表的第一个结点(又称头结点)不存储数据,数据从第二个结点开始存储。列表的第一个元素是链表的第二个结点。

不带头结点链表

链表头结点存储数据。列表的第一个元素也是链表的第一个结点。

定义

定义链表结点,包含数据和指针的结构体。

#ifndef Elemtype
#define Elemtype int
#endif

typedef struct LNode{
	Elemtype data;	// 数据域
	struct LNode *next; // 指针域
} LNode, *LinkList;	// LNode 用于定义结点, LinkList 用于定义链表指针。

基本操作

带头结点的链表,头结点不存储数据,故而无需对头结点指针,即链表指针进行操作。所有数据操作只需对结点上的指针进行操作。

不带头结点的链表,头结点存储数据。需要修改头结点时,如删除头结点,就需要将链表指针指向第二个结点。而链表指针的赋值和结点指针的赋值是不一样的:

#include <malloc.h>
LinkList L;
LNode *p1 = (LNode*)malloc(size(LNode)),
	  *p2 = (LNode*)malloc(size(LNode));

L = p1;	// 链表指针赋值
p->next = p2;// 结点指针赋值

故而需要单独考虑对链表指针的操作。

总结:不带头结点的链表需要额外添加对链表指针(头结点)的操作。

1、初始化

对链表指针进行初始化。

时间复杂度 O(1);

// 初始化一个带头结点的链表,修改链表指针,传引用。
bool LinkListInit(LinkList &L){
    // 创建一个头结点
	L = (LinkList)malloc(sizeof(LNode));
	if (L == NULL)      // 内存不足,创建失败。
		return false;
	L->next = NULL;     // 置空头结点
	return true;
}

// 初始化不带头结点链表
bool LinkListInit(LinkList &L, bool withHeaderNode){
    // 带头结点的链表
    if (withHeaderNode) return LinkListInit(L);
    // 不带头结点链表
    L = NULL;
    return true;
}
2、插入
1)后插

在指定结点后面插入新元素。

办法:先让新结点指向下一个结点,再让当前结点指向新结点即可。

考虑:

1、均是修改结点指针,没有涉及到修改链表指针,两种链表操作一致。不同的是,带头结点的链表可以在第一个元素前面插入新元素,即头结点后插,而不带头结点的链表不行。

2、如果是尾结点,尾结点指针指向NULL,交换指针后,新结点指针为NULL,无需单独考虑。

时间复杂度 O(1);

// 后插
bool LNodeBackInsert(LNode *p, Elemtype e){
    // 创建新节点。
    LNode *s = (LNode*)malloc(sizeof(LNode));   
    if (s == NULL) return false;                 
    // 交换指针、赋值
    s->next = p->next;
    p->next = s;
    s->data = e;
    
    return true;
}
// 兼容
bool LNodeBackInsert(LNode *p, Elemtype e, bool withHeaderNode){
    // 节点指针为空,无法访问链表,毫无意义。
    if (p == NULL) return false;
    // 两种链表后插操作相同。
    return LNodeBackInsert(p, e);
}


2)前插

在指定结点前面插入新元素。

问题:要将新结点放在当前结点的前面,就必须让前一结点的指针域指向新结点才行,但是前一节点无法访问(没有指向前一节点的指针,也没有引用),故而此路不通。

办法:偷天换日,先将新结点插入当前结点后面,然后交换两者数据域,新元素就位于指定元素前面了。虽然偷天换日成功,但也留下了蛛丝马迹:当前结点里装的是新元素,原来的元素被放到下一个结点了。

考虑:只修改结点指针域,而非链表指针,两种链表操作相同。注意不能给头结点前插(带头结点链表)。

时间复杂度 O(1);

// 前插
// 详细流程
bool _LNodePreInsert(LNode *p, Elemtype e){
    // 创建新节点。
    LNode *s = (LNode*)malloc(sizeof(LNode));   
    if (s == NULL || p == NULL ) return false;                
    // 交换指针
    s->next = p->next;
    p->next = s;
    // 交换值
    s->data = p->data;
    p->data = e;
    
    return true;
}

// 简写:前插与后插操作只有最后的值交换不同,故而可以调用后插函数简化前插代码
bool LNodePreInsert(LNode *p, Elemtype e){
    // 在新节点中插入当前结点中的元素,可能插入失败
    if (!LNodeBackInsert(p, p->data)) return false;
    // 在当前节点插入新元素
    p->data = e;
    
    return true;
}

// 兼容
bool LNodePreInsert(LNode *p, Elemtype e, bool withHeaderNode){
    // 节点指针为空,无法访问链表,毫无意义。
    if (p == NULL) return false;
    
    // 两种链表前插操作相同。
    return LNodePreInsert(p, e);
}
3)头插

在链表头部插入新元素。

办法:头插是前插的一个特例,先获取第一个元素的指针,然后直接引用前插代码。

考虑:1、空链表的情况:带头结的链表,新结点插入头结点后面,因此可以换成后插操作;不带头结点的链表,将链表指针指向新结点,此法即使不是空链表也适用,因此无需调用前插函数了。

时间复杂度 O(1);

// 头插,修改链表指针,L传引用
bool LNodeHeadInsert(LinkList &L, Elemtype e, bool withHeaderNode){
    // 带头结点的链表
    if (withHeaderNode) return LNodeBackInsert(L, e);
    
    // 不带头结点链表
    LNode *s = (LNode*)malloc(sizeof(LNode));
    if (s==NULL) 
        return false;
    s->next = L;
    s->data = e;
    L = s;
    return true; 
}
4)尾插

在链表尾部插入新结点。

办法:后插特例,可先寻到最后一个元素,然后调用后插函数将新结点插入链尾。

考虑:空链表情况:带头结点带头结的链表,新结点插入头结点后面,无需额外操作。不带头结点的链表,需将链表指针指向新结点,与前插操作相同。

时间复杂度 O(n);

// 尾插(带头结点)
bool LNodeRearInsert(LinkList L, Elemtype e){
    LNode *p = L;
    // 检查参数
    if ( p == NULL ) return false;
    // 不是最后一个结点的指针域不为NULL
    while(p->next != NULL) p = p->next;
    // 调用后插函数
    return LNodeBackInsert(p, e);
}

// 尾插(兼容)。修改链表指针,L传引用
bool LNodeRearInsert(LinkList &L, Elemtype e, bool withHeaderNode){
    // 带头结点的链表
    if (withHeaderNode)
        return LNodeRearInsert(L, e);
    
    // 不带头结点链表
    // 空链表,尾插和前插操作一样。
    if (L==NULL)
        return LNodeHeadInsert(L, e, false);
    // 有结点的链表,后插操作都一样。
    return LNodeRearInsert(L, e);
}

3、删除
1)删除指定结点

问题:删除当前结点,需修改前一结点的指针域,但没有能访问前一结点的指针。

办法:偷天换日。不删除当前结点,而删除后一结点,将后一结点元素,赋给当前结点。这样当前结点的元素就被删除了。

考虑:1、如果当前结点是尾结点,后面没有元素,便无法使用以上方法,只能通过链表指针遍历到前一结点。2、删除结点时,链表不能为空,但链表可以有一个结点。带头结点的链表情况同删尾结点,不带头结点链表,需修改链表指针为NULL。3、当前结点是头结点,偷天换日删除头结点后一个结点,头结点不变,链表指针不变。无需单独考虑。

时间复杂度 O(n);

// 带头结点情况,删除指定结点(结点只是容器,真正要删除的还是元素。)
bool LNodeDeleteByPoint(LinkList L, LNode *p){
    LNode *t; // 临时指针,用于释放被删除的后继结点
    // 检查参数
    if(L == NULL || L->next == NULL || p == NULL) return false;
    // 非尾结点,用后一元素覆盖当前结点的元素,再删除后一结点。就把当前元素删除了。
    if (p->next){
        p->data = p->next->data;
        t = p->next;
        p->next = p->next->next;
    }else{
        // 尾结点只能遍历到链表的倒数第二个结点
        t = p; p = L;
        while(p->next==t){
            p = p->next;
            if (p==NULL) return false; // 遍历完整条链表都没找到指定结点。
        }
        // 新的尾结点
        p->next = NULL;
    }
    free(t);
    return true;
}

// 兼容,删除唯一的结点,需修改链表指针,传引用。
bool LNodeDeleteByPoint(LinkList &L, LNode *p, bool withHeaderNode){
    // 检查参数。
    if (L == NULL || p == NULL ) return false;
    // 带头结点的链表
    if (withHeaderNode) return LNodeDeleteByPoint(L, p);
    
    // 不带头结点的链表
    // 只有一个结点的情况
    if (L->next==NULL ){
        if (L != p) return false; // 不是链表的节点
        free(L); // 释放内存。
        L == NULL;
        return true;
    }
    // 多个结点,删除操作相同。
    return LNodeDeleteByPoint(L, p);
    
}
2)按位序删除结点

指定删除第 n 个结点。

只能遍历链表到第 n-1 个元素,修改其后继。

考虑:1、最多遍历到倒数第二个结点,否则位序错误。2、删除头结点,只有不带头结点的链表才能删除头结点,此时需将链表指针指向头结点的下一项(元素或NULL)。

时间复杂度 O(n);

// 7、按位序删除结点
bool LNodeDeleteByRank(LinkList L, int i){
	// 检查参数
    if (L == NULL || i < 1 ) return false;
    // p 指向要删除结点的前驱,s 指向要删除的结点,在结点脱链后释放结点。
    LNode *p=L, *s;
    // i 为 1 时,p 为头结点,正好是第一个元素的前驱,无需循环。
    while (--i){          
        p = p->next;
        if (p->next==NULL)return false;   // 已经是尾结点了,i已经超限。
    }
    // p 为前驱,s为要删除的结点
    s = p->next; 
    p->next = s->next;
    free(s);
}

// 兼容,修改链表指针,传引用
bool LNodeDeleteByRank(LinkList &L, int i, bool withHeaderNode){
    // 检查参数
    if (L == NULL || i < 1 ) return false;
    // 带头结点的链表
    if (withHeaderNode) return LNodeDeleteByRank(L, i);
    
    // 不带头结点的链表
    // 删除的是头结点,或删除后为空表,也就是头结点后面为空。可见两者可归纳为同种情况。
    if (i == 1){
        L = L->next;
        return true;
    }
    
    // 删除的不是头结点,操作相同,不过不带头结点的第二个元素是带头结点链表的第一个元素。
    return LNodeDeleteByRank(L, i-1);
}

4、查找
1)按位序查找

查找并获取第 n 个元素。

只能遍历链表。

考虑:由于需要知道查找是否成功以及查找到的元素值。宜将是否查找成功作为函数返回值,而元素值则通过传址或传引用的方式获取。

时间复杂度 O(n);

bool LNodeGetByRank(LinkList L, int i, Elemtype &e){
    // 检查参数
    if (i<1 || L == NULL || L->next == NULL) return false; // i 不合法
    LNode *p=L;
    // i 为 1 时,循环一次,p 变为第一个结点。
    while (i--){    
        p = p->next;
        if (p==NULL) return false;   // 已过尾结点,i 已经超限。
    }
    e = p->data;
    return true;
}

// 兼容
bool LNodeGetByRank(LinkList L, int i, Elemtype &e, bool withHeaderNode){
    // 检查参数
    if (L == NULL || i < 1 ) return false;
    // 带头结点的链表
    if (withHeaderNode) return LNodeGetByRank(L, i, e);
    
    // 查找头结点
    if (i == 1) {
    	e = L->data;
    	return true;
	}
	
    // 不是头结点
    return LNodeGetByRank(L, i-1, e);
}
2)按值查找

查找链表中是否有指定值,如果有获取其位序。

只能遍历链表,不带头结点的链表,还需要比较头结点的值。

时间复杂度 O(n);

// 按值查找
int LNodeGetByValue(LinkList L, Elemtype value){
    // 检查参数
    if (L == NULL || L->next == NULL) return false;
    LNode *p = L->next;	// 指向第一个元素
    int i = 1;
    // 第一个元素为 value 时,循环不执行,i 为 1;
    while (p->data != value){
        if (p->next==NULL) return 0;   // p 的后继为 NULL,链表遍历结束,终止循环。
        p = p->next;
        i++;
    }
    return i;
}

// 9、按值查找
int LNodeGetByValue(LinkList L, Elemtype value, bool withHeaderNode){
    // 检查参数
    if (L == NULL) return false;
    // 带头结点的链表
    if (withHeaderNode) return LNodeGetByValue(L, value);
    
    // 不带头结点的链表,比较头结点
    if (value == L->data) return 1;
    
    // 不是头结点,返回结果需加1,才是不带头结点的位序。
    return LNodeGetByValue(L, value)+1;
}
5、批量插入表头

使用头插法插入表头O(1),插入n个数据需要O(n)的时间。

bool LNodeHeadInsertValues(LinkList &L, Elemtype *arry, int len, bool withHeaderNode){
    for (int i=0; i<len; i++){
        if (!LNodeHeadInsert(L, *(arry+i), withHeaderNode))
            return false;   // 插入失败
    }
    return true;
}
6、批量插入表尾

如果每插入一个数据就调用尾插函数O(n), 插入n个数据就需要O(n2)的时间。但如果有尾指针,就能一步插入到表尾了。要获取表尾指针,就需要先遍历链表O(n),再插入元素,后移尾指针即可O(n), 两者相加仍为O(n)。

考虑:插入元素到无结点的链表,和头插法一样。有结点的链表插入操作相同。不要忘了给尾结点指针域置NULL。否则链表就无限长了。

bool LNodeRearInsertValues(LinkList &L, Elemtype *arry, int len, bool withHeaderNode){
    int i=0;
    if (L == NULL) // 空链表
        if(!LNodeHeadInsert(L, *(arry+i++), withHeaderNode))
            return false; // 插入第一个元素失败
    // 遍历到表尾
	LNode *p = L;    
    while(p->next) p = p->next;
    for (LNode *s; i<len; i++){
        s = (LNode*)malloc(sizeof(LNode));  // 创建新结点
        if (s == NULL) return false;
        s->data = arry[i];  
		s->next = NULL;  
        p->next = s;    // 尾结点指向新结点
        p = s;  // 新结点变成尾结点
    }
    return true;
}
posted @ 2021-10-29 17:31  流水自净  阅读(79)  评论(0编辑  收藏  举报