【数据结构】三、单链表的定义和基本操作的实现

数据结构 DATA STRUCTURE

二、线性表

2.1 线性表的定义和基本操作概述

线性表的定义和基本操作

2.2 线性表的顺序表示

推荐阅读:顺序表的定义和基本操作的实现

2.3 线性表的链式表示

线性表的链式表示总结

2.3.1 单链表

一、单链表存储结构描述和特点

线性表的链式存储,通常用指针来描述线性表的链式存储。

通过一组任意的存储单元来存储线性表中的数据元素,通过“链”建立起数据元素之间的关系——为建立数据元素之间的 线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。

1. 单链表存储结构描述
typedef struct LNode {
    
    		// 定义单链表结点类型(链表就是一个个结点通过结点的指针域连起来)
    Elemtype data;			// 数据域
    struct LNode * next;		// 指针域,注意这里定义为 struct LNode *类型,因为还没重命名(不能直接用LNode*)。
}LNode,* LinkList;
  1. 为什么要用 typedef?

    结构体使用方法:struct LNode L;

    重命名之后使用方法:LNode *L;LinkList L;

    使用更加方便。

  2. 为什么要用两个重命名(LNode 和 LinkList),有什么区别?

    拆开看:

    typedef struct LNode {
           
           		
        Elemtype data;			
        struct LNode *next;		
    }LNode;
    

    重命名 struct LNode 结构体类型为 LNode

    typedef struct LNode {
           
           		
        Elemtype data;			
        struct LNode *next;		
    }* LinkList;
    

    重命名 struct LNode* 结构体指针类型为 LinkList

    使用方法不一样:LNode *L;LinkList L;但作用完全相同,后者可读性更强。(用于强调——前者强调这是个结点,后者强调这是一个表。)

typedef
image-20220926102009295
typedef3


2. 单链表特点

优:

  • 不需要使用地址连续的存储单元(不要求逻辑上相邻的元素在物理位置上也相邻)
  • 插入删除操作快(不需要移动元素,修改指针即可)

缺:

  • 不能随机存取,只能顺序存取。(每一个结点只认识后继结点)
  • 附加指针域,浪费空间

3. 单链表的一些概念

头指针:用来标识单链表,值为NULL时表示为空表。

头结点:为了操作上的方便,在单链表第一个结点之前附加一个结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。

头指针和头结点区分:不管带不带头结点,头指针都始终指向链表的第一个结点;而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。

引入头结点优点

  • 使链表在第一个位置上的操作和在表的其他位置上的操作一致,无需特殊处理。
  • 使空表和非空表的处理统一。



二、单链表基本操作实现
1. 创建单链表(初始化)

(1)头插法

从空表开始,建立头结点,然后循环将新结点插入到表头(头结点的后一个位置)来创建单链表

Steps:

  1. 创建头结点
  2. 一直在且只在头结点和第一个结点之间的位置插入结点
// 头插法建立单链表
LinkList List_HeadInsert(LinkList &L) {
    
    
	// Variables Definition
	LNode *s; 									// 中间变量,临时保存结点 
	int x;										// 中间变量,临时保存输入的数据 
	// Create HeadNode
	L = (LinkList)malloc(sizeof(LNode));		// 创建头结点(由系统生成一个LNode型的结点,同时将该结点的起始位置赋给指针变量s。) 
	// Initialization
	L->next = NULL; 							// 初始化为空链表
	// Input
	printf("Input: \n");
	scanf("%d", &x);	 
	// 头插 (在头结点后一位置一直插入,也是当前结点的前一个位置)
	while (x != 9999) {
    
    							// Input 9999 means the end
		s = (LNode *)malloc(sizeof(LNode));		// 1. 指针需要动态分配存储  
		// Assignment 
		s->data = x;							// 2. 先将input的x赋值到结点
		// Insert 
		s->next = L->next;						// 3. 先接管人家儿子 
		L->next = s;							// 4. 再做人家新的儿子 
		// Next Input
		scanf("%d", &x);					
	}
	// Return
	return L;									// 返回生成的单链表 
}

Questions:

  1. s->data 什么意思?

    s->data == (*s).data

时间复杂度:每个结点插入的时间为 O(1),总时间复杂度为O(n)。


(2)尾插法

增设尾指针 r,循环将新结点插入到表尾并更新尾指针 来创建单链表。

Steps:

  1. 创建头结点,并设置当前指针s,尾指针r
  2. 循环创建结点并接到r后面。
  3. 更新r(将r指向新的尾结点)
  4. 尾结点指针置空
// 尾插法建立单链表
LinkList List_TailInsert(LinkList &L) {
    
    
	// Variables Definition
	int x;
	L = (LinkList)malloc(sizeof(LNode));
	LNode *s, *r = L;							// r--tailpointer
	// Input
	printf("Input: \n");
	scanf("%d", &x);							// Input
	// TailInsert
	while (x != 9999) {
    
    
		s = (LNode *)malloc(sizeof(LNode));		// Create Temporary-Node 
		// Assignment
		s->data = x;							
		// Insert(Link to the Temp node)
		r->next = s;
		// Move the tailpointer
		r = s;
		// Next Input
		scanf("%d", &x);	
	}
	r->next = NULL;
	// Return
	return L;		
} 

Summary:

  1. 头插 VS 尾插

    因为是后插(尾插用的就是只在尾巴后插)操作,直接 r->next = s; 即可,比前插简单多了。(其实也可以 s->next = r->next,但是没必要。因为最后直接进行尾结点指针置空(r->next = NULL)就行,对所有要后插的结点来说,都是这样,所以,等插入完毕之后,来一句 r->next = NULL 就OK了。)(就像有素质的在最后面排队一样)

    而头插就不行了,因为他们后面的结点是随时更新的。(就像没素质的插队一样)

  2. 顺序表 VS 链表

    • 为什么前面讲的顺序表没有这样边输入边创建?

      那是因为本来顺序表就是提前申请一片连续空间来创建的,不会像这样”边输入边创建“呀。

      你非要输入进行初始化的话,设置个变量直接存进L.data[i]就行(而链表就需要不断移动指针,然后p->data)。(没错,链表的操作就是比顺序表繁琐一丢丢。但是换来的是插入和删除场景下时间上的提高。)

时间复杂度:每个结点插入的时间为 O(1),总时间复杂度为O(n)。


(3)根据 传入数组 初始化单链表,用尾插法实现

将尾插法中的“输入并赋值”部分,改为赋值为传入的数组。

Steps:

  1. 为定义的单链表L分配空间,创建头结点
  2. 设置一个尾指针,初始指向头结点
  3. 根据传进来的参数len,循环创建新结点p,并将p->data赋值为Arr[i]
  4. 将p链接到r屁股后面
  5. 最后将尾结点指针置空
// 赋值(根据传入数组尾插法“创建”单链表) 
void ListAssign(LinkList &L, ElemType Arr[], int len) {
    
        // 由于形参数组只是个指针,无法传送求数组长度信息,所以只能添加数组长度参数
    L = (LNode *)malloc(sizeof(LNode));
	int i;
	LNode *r = L;								// 尾指针 
	for (i = 0; i < len; i++) {
    
    
		LNode *p = (LNode *)malloc(sizeof(LNode));
		p->data = Arr[i];						// 赋值 
		r->next = p;							// 将 p 链接到 尾指针 
		r = p; 									// 移动尾指针 
	}
	r->next = NULL;								// 尾结点指针置空  
}

Questions:为什么要 L = (LNode *)malloc(sizeof(LNode));

Answer:传入的 L 只是用 LinkList L;定义了一下,也只是个指针,一个没有内容的指针,所以在初始化的时候要先 L = (LNode *)malloc(sizeof(LNode)); 进行赋值奥。


(4)初始化为空表

分配头结点,并且设置头结点指针域为空。

// 初始化空单链表
bool ListInit(LinkList &L) {
    
    
	L = (LNode *)malloc(sizeof(LNode));			// 分配一个头结点(不带头结点的话不用这个 ) 
	if (L == NULL)								// 内存不足分配失败 
		return false;
	L->next = NULL;
	return true;
}

2. 插入操作

在表 L 的 i 位置插入 e。

Steps:

先检查插入位置的合理性,然后找到待插入结点的前驱节点(i-1),最后插入新结点。

  1. 检查 i 是否合理
  2. 获取第 i-1 个结点的指针
  3. 创建新结点,并且将 e 存放到结点的数据域
  4. 插入
    1. 接管 第i-1个结点 的儿子 :s->next = p->next;
    2. 成为 第i-1个结点 的儿子 :p->next = s;
  5. return
// 插入
bool ListInsert(LinkList &L, int i, ElemType e) {
    
    
	// Judge whether i is valid
	if ((i < 1) || (i > Length(L)+1))
		return false;
	// Get the i-1 node
	LNode *p = GetElem(L, i-1);					// 调用按序查找函数 
	// Create Temporary-Node and Assign INPUT to s->data 
	LNode *s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	// Insert
	s->next = p->next;
	p->next = s;
	// return 
	return true;
} 

插入操作总是先拿到没有“直接指针”的结点,然后再去搞那些有“直接指针”的。(防止没有“直接指针”的结点丢失。)


3. 删除操作

删除表 L 第 i 位置元素,并将其存放到全局变量e。

Steps:

  1. 检查 i 是否合理
  2. 获取第 i-1 个结点的指针
  3. 将 i 结点的值 p->next->data 放到 e
  4. 删除 i 结点:令 第i-1结点 的next指针指向 第i+1结点(i->next) 即可。

get 第 i-1 个结点,然后拿到第 i 个结点,最后让 i-1 结点直接指向 i 的 next ,也就是 i+1 。

// 删除
bool ListDelete(LinkList &L, int i, ElemType &e) {
    
    
	if (i<1 || i > Length(L))
		return false;
	LNode *p = GetElem(L, i-1);
	LNode *q = p->next; 
	e = q->data;
	p->next = q->next;
	free(q);
	return true;
} 

Q:为什么有的函数里的LNode指针需要malloc动态分配存储,有的不需要?

A:只有涉及到创建结点的时候需要malloc(像创建、赋值等)。但是像查找、删除等需要用到 LNode指针的情况,就不需要malloc了,因为直接指向目标存储就OK了,这个存储是之前申请过的啦(一般的话,之前用初始化/赋值/创建函数申请了。),直接指向它即可。

例:

LNode *p = GetElem(L, i-1);					// 这个获取第i-1个结点,这个结点之前已经申请了空间了! 
LNode *s = (LNode*)malloc(sizeof(LNode));	// 而 s 这个中间结点,是没有空间的!需要新申请! 

4. 按序查找

获取表 L 第 i 个结点的指针。

Steps:

  1. i 的合法性判断
  2. 初始化计数器、拿到第一个节点
  3. 只要 p(当前结点) 不为空,且 计数器 < i ,就循环 p->nextj-1。(把自己想if的东西可以直接放到循环里:if(j=i)
  4. 如果 j>=i ,会返回p->next(最后一个结点的next指针),为NULL。
// 按序号查找结点
LNode *GetElem(LinkList L, int i) {
    
    
	int j = 1;									// counter
	// Assign the FirstNode's pointer to p(TempPoiter)
	LNode *p = L->next;							// FirstNode, not HeadNode							
	// Judge whether i is valid
	if (i == 0)				
		return L;
	if (i < 1)
		return NULL;
	// Search 
	while (p && j < i) {
    
    						// 如果 p(当前结点) 不为空,且 计数器 < i 
		p = p->next;
		j++;
	}
	// Return the pointer(previousNode's pointer domain) of ith node
	return p;									// if j >= i,then NULL(lastNode's pointer domain) will be returned.
} 

Q:为什么不直接返回值?而是一个结点?

A:因为GetElem函数返回结点的话,不仅能返回值,还能让插入,删除函数使用。(因地制宜,按需设计)


5. 按值查找

获取表 L 中值为 e 的结点的指针。

Steps:

循环找下一个是不是 e,循环最后没找到返回的 p 的指针域就是 NULL

  1. 拿到第一个结点
  2. 只要 p 不为空 且 p->data 不等于 e,就循环 p=p->next
  3. 返回 p
// 按值查找
LNode *LocateElem(LinkList L, ElemType e) {
    
    
	LNode *p = L->next;
	while (p != NULL && p->data != e)
		p = p->next;
	return p;
}

6. 判空

判断头结点的指针域是否为空。

// 判空
bool Empty(LinkList L) {
    
    
//	if (L->next == NULL)
//		return true;
//	else 
//		return false;
	return (L->next == NULL);  // 一句式写法,666 
}  

7. 求表长

求表 L 的长度。

Steps:用计数器变量计数,p不为空则一直 next。

// 求表长
int Length(LinkList L) {
    
    
	int len = 0;
	LNode *p = L->next;
	while (p) {
    
    
		len++; 
		p = p->next;
	}		
	return len;
} 

8. 打印表

Steps:循环 printf,并且 next。

// 打印 
void ListPrint(LinkList L) {
    
    
	LNode *p = L->next;
	while(p)
		printf("%d ", p->data);
	printf("\n");
}

9. Else

1. 前插

在给定结点 p 前插入结点 s。

单链表的算法中,通常采用 后插 操作。前插操作的话,需要找到前驱结点,再进行插入,时间复杂度高 O(n)。那有没有一种O(1)的前插算法呢?

Steps:后插,然后交换数据域。(“偷天换日”)

// 后插
s->next = p->next;
p->next = s;
// 交换数据域
temp = p->data;
p->data = s->data;
s->data = temp;

2. 删除指定结点

删除给定结点 p 。

找前驱结点,然后删除?O(n)——给的是待删除结点p,没有p的前驱结点,传统操作是删不了p的……

Steps:拿到后继结点的值,然后删除后继结点。(“找替死鬼”)

q = p->next;
p->data = p->next->data;			// 拿到后继结点数据
p->next = q->next;					// 抛弃后继结点
free(q);

单链表总结
  1. LinkList

    • 插入操作很核心
      1. 接管人家的儿子
      2. 做人家儿子
    • 删除操作直接链接到后继结点的next。
    • 头插尾插创建,都涉及到插入操作。
    • 注意创建LinkList之前需要初始化(主要是让L->next=NULL)。
  2. HeadInsert VS TailInsert

    Item HeadInsert TailInsert
    总体 头插麻烦一点,插入麻烦一丢丢 尾插简单,就是多设置一个尾指针,往后链接就OK。
    时间复杂度 O(n) O(n)
    输入数据顺序 和 生成的链表中的元素顺序 相反 相同

    虽然结点插入时间复杂度是O(1),但是需要插入n次,故两个插入创建链表方式时间复杂度都是O(n),因为需要循环一个个赋值,(好像不管什数据结构,创建操作都是O(n)吧)

  3. GetElem VS LocateElem

    • 按值查找代码很少,少的就是关于 计数器变量 的部分。(计数器变量的定义,有效性判断以及使用)
    • 时间复杂度都是 O(n)。(因为链表的数据结构,无法直接拿到目标,需要一个个查)
  4. p=L VS p=L->next
    就是设置不同的起始点而已。



三、顺序表、单链表总结

一、选出最优数据结构:

  1. 存取第 i 个元素及其前驱和后继元素的值

    顺序表 O(1)

  2. 交换第3个元素和第4个元素的值

    顺序表 O(1)

  3. 顺序输出表内所有值

    顺序表 = 单链表 = O(n)

  4. 给出指定点,在指定点前插/后插

    单链表 O(1)

二、算法思想总结

  1. 写一个函数需要关注的点:参数及参数是否需要引用,返回值及返回值类型。

    函数代码一般包括:

    1. 合法性判断
    2. 良好的return
    3. 核心逻辑算法
  2. 最小/大值一般都是定义一个变量保存 数组的第一个元素 为初始值,然后循环和剩下的比较并进行更新。

  3. 倒置这种对整个表所有元素的操作,一般处理一半表就可以了。处理一半表,就是循环到 L.length/2。

  4. 删除所有x,删除操作一般都要考虑空间复杂度,可以优化到 O(1) 。

三、顺序表 VS 链表

  1. 优缺点对比

    顺序表 单链表
    1. 随机存取:通过 首地址 和 元素序号 可在时间 O(1) 内找到指定的元素。
    2. 存储密度高:每个结点只存储数据元素。
    1. 不需要使用地址连续的存储单元(不要求逻辑上相邻的元素在物理位置上也相邻)
    2. 插入删除操作快(不需要移动大量元素,修改指针即可)
    1. 静态分配数组拓展容量不方便,动态分配数组方式时间复杂度高。
    2. 插入和删除操作需要移动大量元素。
    3. 需要使用一整块地址连续的存储单元。(逻辑相邻的物理也相邻)
    1. 不能随机存取,只能顺序存取。(每一个结点只认识后继结点)
    2. 附加指针域,浪费空间。
  2. 操作对比

    Item SqList LinkList
    算法设计 一般用 数组L.data[i]、for循环 和 i循环变量,以及L.length来进行算法设计; 一般用 指针、while循环、s->next、以及p/p->next是否==NULL进行算法设计;
    (头结点不可或缺总会用到,而且没前驱结点什么也干不了,而且新建结点需要手动动态分配内存–malloc)
    插入 先挪开位置,后数组的赋值进行插入。 先赋值完数据域,后设置指针域进行连接。
    删除 值的覆盖即删除(目标元素后所有元素前移1位) 让值在的结点脱链就是删除(越过待删结点链接到待删结点后面)
    拿第i个元素 直接对数组进行操作。data[i]。 链表操作之前总是需要拿到第一个结点,然后一直next到 前驱结点。

    在我看来,数组也是“二级数据结构”,也是一种数据类型的封装,所以顺序表本来就站在一个至高点上。

  3. 效率对比

    Operations SqList LinkList
    插入 O(n) O(n)
    给定前驱结点的插入 O(n) O(1)
    删除 O(n) O(n)
    给定前驱结点的删除 O(n) O(1)
    按值查找 O(n) O(n)
    按序查找 O(1) O(n)

    以上时间复杂度为对应操作的平均时间复杂度

单链表基本操作测试(可直接运行)

代码:

#include <stdio.h>
#include <stdlib.h>
/*
 * 单链表的存储结构定义 
 */
#define ElemType int    		// 定义线性表元素数据类型 

typedef struct LNode{
    
    		// 定义单链表结点类型(链表就是一个个结点通过结点的指针域连起来)
    ElemType data;			// 数据域
    struct LNode *next;		// 指针域
}LNode, *LinkList;			// LNode * = LinkList, 前者强调这是个结点,后者强调这是个链表,完全一样 



/*
 * 单链表的基本操作实现
 */

// 初始化空单链表 (分配头结点,并且设置头结点指针域为空。)
bool ListInit(LinkList &L) {
    
    
	L = (LNode *)malloc(sizeof(LNode));			// 分配一个头结点(不带头结点的话不用这个 ) 
	if (L == NULL)								// 内存不足分配失败 
		return false;
	L->next = NULL;
	return true;
}


// 赋值 (根据传入数组尾插法“创建”单链表)
void ListAssign(LinkList &L, ElemType Arr[], int len) {
    
        // 由于形参数组只是个指针,无法传送求数组长度信息,所以只能添加数组长度参数 
	L = (LNode *)malloc(sizeof(LNode));
	int i;
	LNode *r = L;								// 尾指针 
	for (i = 0; i < len; i++) {
    
    
		LNode *p = (LNode *)malloc(sizeof(LNode));
		p->data = Arr[i];						// 赋值 
		// p->next = r->next;
		r->next = p;							// 将 p 链接到 尾指针 
		r = p; 									// 移动尾指针 
	}
	r->next = NULL;								// 尾结点指针置空  
}


// 判空 (判断头结点的指针域是否为空。)
bool Empty(LinkList L) {
    
    
//	if (L->next == NULL)
//		return true;
//	else 
//		return false;
	return (L->next == NULL); 
}   


// 头插法建立单链表(从空表开始,建立头结点,然后将新结点插入到表头--头结点之后) 
LinkList List_HeadInsert(LinkList &L) {
    
    
	// Variables Definition
	LNode *s; 									// 中间变量,临时保存结点 
	int x;										// 中间变量,临时保存输入的数据 
	// Create HeadNode
	L = (LinkList)malloc(sizeof(LNode));		// 创建头结点(由系统生成一个LNode型的结点,同时将该结点的起始位置赋给指针变量s。) 
	// Initialization
	L->next = NULL; 							// 初始化为空链表
	// Input
	printf("Input: \n");
	scanf("%d", &x);	 
	// 头插 (在头结点后一位置一直插入,也是当前结点的前一个位置)
	while (x != 9999) {
    
    							// Input 9999 means the end
		s = (LNode*)malloc(sizeof(LNode));		// 1. 指针需要动态分配存储  
		// Assignment 
		s->data = x;							// 2. 先将input的x赋值到结点
		// Insert 
		s->next = L->next;						// 3. 先接管人家儿子 
		L->next = s;							// 4. 再做人家新的儿子 
		// Next Input
		scanf("%d", &x);					
	}
	// Return
	return L;									// 返回生成的单链表 
} 


// 尾插法建立单链表(将新结点插入到表尾,增设尾指针 r)
LinkList List_TailInsert(LinkList &L) {
    
    
	// Variables Definition
	int x;
	L = (LinkList)malloc(sizeof(LNode));
	LNode *s, *r = L;							// r--tailpointer
	// Input
	printf("Input: \n");
	scanf("%d", &x);							// Input
	// TailInsert
	while (x != 9999){
    
    
		s = (LNode *)malloc(sizeof(LNode));		// Create Temporary-Node 
		// Assignment
		s->data = x;							
		// Insert(Link to the Temp node)
		r->next = s;
		// Move the tailpointer
		r = s;
		// Next Input
		scanf("%d", &x);	
	}
	r->next = NULL;
	// Return
	return L;		
} 
/*
对于链表的创建,头插是一直循环用s(固定位置不需要移动指针),尾插是设置尾指针r且一直移动r实现遍历 
尾插法如果不设置尾指针的话,每次插入都需要遍历得到前去结点,时间复杂度为 O(n2)。 
*/


// 插入(先检查插入位置的合理性,然后找到待插入结点的前驱节点(i-1),最后插入新结点)
bool ListInsert(LinkList &L, int i, ElemType e) {
    
    
	// Judge whether i is valid
	if ((i < 1) || (i > Length(L)+1))
		return false;
	// Get the i-1st node
	LNode *p = GetElem(L, i-1);
	// Create Temporary-Node and Assign INPUT to s->data 
	LNode *s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	// Insert
	s->next = p->next;
	p->next = s;
	// return 
	return true;
} 


 不带头结点的单链表按位序插入
//if (i == 1) {
    
    
//	LNode *s = (LNode*)malloc(sizeof(LNode));
//	s->data = e;
//	s->next = L;			// 不带头结点的头部插入和其他地方插入不一样,没有前驱结点,需要移动L指针。 
//	L = s;
//	return true;	
//}
	
 
// 指定结点后插
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;
	s->next = p->next;
	p->next = s;
	return true;
} 


// 指定结点的前插操作(偷天换日法)
bool InsertPriorNode(LNode *p, ElemType e) {
    
    
	if (p == NULL)
		return false;
	LNode *s = (LNode *)malloc(sizeof(LNode));
	if (s == NULL)
		return false;
	s->next = p->next;		// Insert
	p->next = s;
	s->data = p->data;		// 将p中元素复制到s 
	p->data = e; 			// p中元素覆盖为e 
	return true; 
}
 

// 删除 (get第i-1个结点,然后拿到第i个结点,最后让i-1结点直接指向i的next,也就是i+1) 
bool ListDelete(LinkList &L, int i, ElemType &e) {
    
    
	if (i<1 || i > Length(L))
		return false;
	LNode *p = GetElem(L, i-1);
	LNode *q = p->next; 
	e = q->data;
	p->next = q->next;
	free(q);
	return true;
} 

 
// 按序号查找结点(从头结点开始,p->next到i-1)(p=L->next版)
LNode *GetElem(LinkList L, int i) {
    
    
	int j = 1;									// counter
	// Assign the FirstNode's pointer to p(TempPointer)
	LNode *p = L->next;							// FirstNode, not HeadNode							
	// Judge whether i is valid
	if (i == 0)				
		return L;
	if (i < 1)
		return NULL;
	// Search 
	while (j < i && p) {
    
    						// 如果 p(当前结点) 不为空,且 计数器 < i ;while (p && j < i)--这样效率高一点,前者可读性更高 
		p = p->next;
		j++;
	}
	// Return the pointer(previousNode's pointer domain) of ith node
	return p;									// if j >= i,then NULL(lastNode's pointer domain) will be returned.
} 
/*
1. 为什么不返回值?而是一个结点?
因为GetElem函数返回结点的话,不仅能返回值,还能让插入函数和删除函数使用。(因地制宜、按需设计) 

2. 如果p=L,那么需要j=0(j表示p指向的是第几个结点)。 
*/


// 按序号查找(p=L版,个人觉得这个更改、更简洁)
LNode *GetElem_2(LinkList L, int i) {
    
    
	if (i < 0)
		return NULL;
	LNode *p = L;
	int j = 0;
	while (p!=NULL && j<i) {
    
    
		p = p->next;
		j++;
	}
	return p;
}
 

// 按值查找(循环找下一个是不是e,循环最后没找到的话返回的p的指针域就是NULL) 
LNode *LocateElem(LinkList L, ElemType e) {
    
    
	LNode *p = L->next;
	while (p != NULL && p->data != e)
		p = p->next;
	return p;		// 循环最后没找到的话返回的尾结点p的指针域就是NULL
}


// 求表长(用计数器变量计数,一直next)
int Length(LinkList L) {
    
    
	int len = 0;
	LNode *p = L->next;
	while (p) {
    
    
		len++; 
		p = p->next;
	}		
	return len;
} 


// 打印 (循环输出,并且next)
void ListPrint(LinkList L) {
    
    
	if (Empty(L))
		printf("The List is empty.\n"); 
	else {
    
    
		LNode *p = L->next;
		while(p) {
    
    
			printf("%d ", p->data);
			p = p->next;
		}		
		printf("\n");	
	}
}


/*
 * main进行测试
 */
int main()
{
    
    
	// Definition
	LinkList list1, list2, list3;
	ElemType e, a[10]={
    
    1,2,3,4,5,6,7,8,9,10};
	int i = 0;
	/*********************Test*********************/ 
	// Init2EmptyList
	ListInit(list3);
	ListPrint(list3); 
	// Create by array
	ListAssign(list3, a, 10); 
	ListPrint(list3);
	// HeadInsert&TailInsert Initialization
	printf("HeadINSERT Initialization Beginning...\n"); 
	list1 = List_HeadInsert(list1);
	printf("HeadINSERT Initialization Finished.\n");
	printf("TailINSERT Initialization Beginning...\n"); 
	List_TailInsert(list2);
	printf("TailINSERT Initialization Finished.\n"); 
	// ListPrint
	ListPrint(list1);
	ListPrint(list2); 
	// LengthPrint
	printf("list1's length: %d\n", Length(list1));
	printf("list2's length: %d\n", Length(list2));
	// Insert
	printf("LISTINSERT Beginning...\n"); 
	printf("Input the Position and Value you want INSERT:\n");
	scanf("%d%d", &i, &e);
	ListInsert(list1, i, e);
	printf("LISTINSERT Finished.\n");
	ListPrint(list1);
	// Delete
	printf("DELETE Beginning...\n");
	printf("Input the value\'s location you want DELETE:\n");
	scanf("%d", &i);
	ListDelete(list1, i, e);
	ListPrint(list1);
	// Read
	printf("GET Beginning...\n");
	printf("Input the value\'s location you want GET:\n");
	scanf("%d", &i);
	LNode *p = GetElem(list1, i); 
	printf("The %dthNode is %d\n", i, p->data);
	
	printf("LOCATE Beginning...\n");
	p = LocateElem(list1, 666);
	if (p)
		printf("666 is in List.\n");
	else
		printf("666 is not in List.\n");
	/*********************End**********************/
	// Return 
	return 0;
} 

运行结果:
result


2.3.2 双链表

一、双链表存储结构描述和特点

存储结构描述:

typedef struct DLNode {
    
    
    ElemType data;
    struct DLNode *prior, *next;		// 前驱和后继指针(都要带*,Type*不支持同时定义多个变量,除非先typedef)
}DLNode, * DLinkList;

特点:

增设前驱指针,解决单链表只能处理后继结点和必须从头遍历的痛点。

二、双链表基本操作实现

将“去”、“来”看成单链表的一次指向操作。

→+← == →/←来简化记忆。

1. 插入

(1)给定两结点前一结点 *p ,在两结点中间插入结点 s

// "右边"
s->next = p->next;
p->next->prior = s;
// "左边"
s->prior = p;
p->next = s;			// 最后再修改p的next指针域,比较保险!

先处理没有直接指针的一边(此题是右边),再去搞有直接指针操控的一边(此题是左边),防止没有直接指针的一边丢失。

(2)给定两结点后一结点 *p 的插入,在两结点中间插入结点 s

// 左边
s->prior = p->prior;
p->prior->next = s;
// 右边
s->next = p;
p->prior = s;
2. 删除

删除 结点*p 的后继结点 *q。(给了两个结点!)

// 将 q 脱链——越过q,直接链上q->next
p->next = q->next;
q->next->prior = p;
// 释放q
free(q);
双链表基本操作实现
/*
 * 双链表基本操作实现 
 */
#include <stdio.h>
#include <stdlib.h>

// 双链表数据结构声明 
typedef int ElemType;

typedef struct DNode {
    
    
	ElemType data;
	struct DNode *prior, *next;
}DNode, * DLinkList;

// 初始化 
bool InitDLinkList(DLinkList &L) {
    
    
	L = (DNode *)malloc(sizeof(DNode));
	if (L == NULL)
		return false;
	L->prior = NULL;
	L->next = NULL;
	return true;
}


// 判空
bool Empty(DLinkList L) {
    
    
	if (L->next == NULL)
		return true;
	else 
		return false;
}


// 在p结点后插入s结点
bool InsertNextDNode(DNode *p, DNode *s) {
    
    
	// Validity Judgement
	if (p==NULL || s==NULL)
		return false;
	// Insert	 
	s->next = p->next;
	if (p->next != NULL)	// p为尾结点的话,p->next为NULL,没有prior 
		p->next->prior = s;		
	s->prior = p;
	p->next = s;
	// Return
	return true;
} 
/*
核心代码: 
s->next = p->next;
p->next->prior = s;		
s->prior = p;
p->next = s;
*/


// 删除p结点的后继结点
bool DeleteNextDNode(DNode *p) {
    
    
	if (p == NULL)		
		return false;
	DNode *q = p->next;
	if (q == NULL)		// p没有后继 
		return false;
	p->next = q->next;
	if (p->next != NULL)	// q结点不是尾结点 
		q->next->prior = p;
	free(q);
	return true;
} 
/*
核心代码:
p->next = q->next;
q->next->prior = p;
free(q);
*/ 


// 销毁
bool DestoryList(DLinkList &L) {
    
    
	while (L->next != NULL)		// 遍历删除结点 
		DeleteNextDNode(L);
	free(L);	// 释放头结点 
	L = NULL;	
	return true;
} 


// 打印
bool ListPrint(DLinkList L) {
    
    
	DLNode *p = L->next;
	while (p != NULL) {
    
    
		printf("%d ", p->data);
		p = p->next; 
	}
	return true;
} 
/*
遍历:

// 后向遍历 
while (p != NULL) {
	p = p->next; 
}

// 前向遍历 
while (p != NULL) {
	p = p->prior;
}

// 前向遍历(跳过头结点) 
while (p->prior != NULL) {
	p = p->prior;
}
*/



/*
Summary

1. 非循环链表中很多对表尾的插入删除操作都会出现没有前驱问题(p->prior出错),所以在执行对应操作的时候总要进行一些合法性判断,而循环链表则不用担心。 
*/

2.3.3 循环链表

一、循环单链表

尾结点指针不是NULL,而指向头结点。(注意不是第一个元素的结点,是头结点!为了方便操作统一而设置的结点。)

判空条件:头结点是否指向本身(头结点是否等于头指针)

if (L->next == L)

特点:

  • 经常在表头表尾进行操作的话,可以用 带尾指针的循环单链表

  • 可以从表中任意结点开始遍历整个链表。

二、循环双链表

尾结点指向头结点,而且头结点的前驱指向尾结点,尾结点的后继指向头结点。

判空if ((L->next == L) && (L->prior == L))

2.3.4 静态链表

借助数组来实现链式存储,指针域存储下个结点的相对地址(数组下标)。

和顺序表一样,需要预先分配一块连续的内存空间。

#define MaxSize 50
typedef struct {
    
    
    ElemType data;			// 数据
    int next;				// 下一个元素的相对地址(数组下标)
}SLinkList[MaxSize];

特点:

  • next == -1 为结束标志。
  • 插入、删除等操作与动态链表一样,只需要修改指针,不需要修改或移动元素。

静态链表没有单链表使用方便,但是在不支持指针的高级语言(如Basic)中,这是一种非常巧妙的设计方法。


回顾一下吧:
线性表的链式表示总结

猜你喜欢

转载自blog.csdn.net/qq_45770232/article/details/128374007
今日推荐