LinkedList链表(从酒店管理到单链表)

今天说说链表:说到链表就不得不提及数组,两者相爱相杀,但又都是极为重要的基本数据结构类型。

相对于链表,我们一般情况下更熟悉数组。

听说加上英文,会显得高端不少):

先讲一个小故事(虽然讲的很烂2333)

从前,一群小朋友外出游玩,到酒店申请房间啊,他们呢,申请的是数组方式的房间(哈哈)那么他们的房间号就是相连的。他们很容易的相互串门,带队者也可以轻松的找到他们每个人的所在(带队者只用记住第一个小朋友的位置和学号就好)。但问题是,他们这样申请房间的话,有的小朋友如果申请更换房间,会比较麻烦,更有甚者,不小心生病了,需要退房,那么负责人就要重新给每个人好分配房间,大多数小朋友都需要更换自己的房间,就会很麻烦。这时候,酒店管理者想到,如果我们让学号相邻的小朋友的记住他(她)下一个学号的小朋友的房间号(就像一号记住二号的房间号,二号记住三号的房间号),那么负责人还是只用记住第一个小朋友的房间号就可以按照顺序依次找到每个小朋友。(当然要是其中一个小朋友忘记后面小朋友的位置的话。。。我们就失去了一堆小朋友)。酒店管理者发现这样的话,不仅可以使得他们的房间利用率得到大大的提升,并且可以方便住宿人员的调动。

好的,相信读完这个尴尬的小故事,我们大概对链表有了一个感性的认识,他是为了解决一些数组的不足而出现的。下面就是严谨的分析链表的优缺点。

作为有强大功能的链表,对他的操作当然有许多,比如:链表的创建,修改,删除,插入,输出,排序,反序,清空链表的元素,求链表的长度等等。

初学链表,一般从单向链表开始:(本文也暂时只有单向链表)

Advantages 优势

1) Dynamic size(动态大小)

2) Ease of insertion/deletion(方便插入和删除)

Drawbacks 缺点

1) Random access is not allowed. We have to access elements sequentially starting from the first node. So we cannot do binary search with linked lists efficiently with its default implementation.

不允许随机访问。 我们必须从第一个节点开始顺序访问元素。 因此,我们无法使用其默认实现对链接列表进行有效的二分查找。

2) Extra memory space for a pointer is required with each element of the list.

需要额外的空间去存放链表中下一个元素的位置。

3) Not cache friendly. Since array elements are
contiguous locations, there is locality of reference which is not there
in case of linked lists.

不缓存友好。 由于数组元素是连续的位置,因此存在引用位置,而在链接列表的情况下则不存在。

(就是说不能写成a[6])

Representation 表达(内容)

首先,我们将一个链表中的每个元素称为一个节点(Node),特殊的第一个被叫做头节点,最后一个被叫做尾节点。(图中,abcd是四个节点,但注意a不是头节点哈哈,Head是头节点,他指向的a节点)一般来说,头节点Head不存放内容,尾节点没有下一个元素的位置。(以简单的单链表为例)

在这里插入图片描述
A linked list is represented by a pointer to the first node of the
linked list. The first node is called the head. If the linked list is
empty, then the value of the head is NULL.

链接列表由指向链接列表的第一个节点的指针表示。 第一个节点称为头。 如果链表为空,则head的值为NULL。(好绕口
Each node in a list consists of at least two parts:

每一个节点至少包括两部分:

  1. data 数据
  2. Pointer (Or Reference) to the next node 指向下一个结点的指针(指向/引用)
    In C, we can represent a node using structures. Below is an example of a linked list node with integer data.

在c语言中,我们使用结构体来表示,如下面的代码:

struct Node {
int data;
struct Node* next;
};

In Java or C#, LinkedList can be represented as a class and a Node as a
separate class. The LinkedList class contains a reference of Node class
type.

而在java,c#中LinkedList可以表示为一个类,而Node可以表示为单独的类。 LinkedList类包含Node类类型的引用。

class LinkedList { 
	Node head; // head of the list 

	/* Linked list Node*/
	class Node { 
		int data; 
		Node next; 

		// Constructor to create a new node 
		// Next is by default initialized 
		// as null 
		Node(int d) { data = d; } 
	} 
}

写一下链表?

我们从c开始尝试:(现在只写到了增)

好的,在此之前,我们似乎好像可能看起来学会了如何使用,但是看懂了和会写出来中间还是有一定的差距的。

链表还有一下基础的知识需要去掌握:

malloc :用于申请一定的内存.

那么这句(struct Node*)malloc(sizeof(struct Node))是什么鬼呢?(这也太长了吧

不急,我们仔细看看,他是申请了一个内存,多大呢?(struct Node)类型的所需要的储存空间;(就像房间的类型一样,偏要双人床的那种)

那么前面的那个(struct Node*)是?哦,他是说这一块申请来的内存要强制转化成struct Node类型的,就是说专门给节点使用的。(自己辛辛苦苦申请来的,才不给别人用呢)。

对了,这句话是会返回一个地址的,就是指向自己申请内存的位置。

注意:节点呀!只有Head和普通节点。我们说的头指针,尾指针一类的,都是仅仅声明,作用就像房卡一样,是可以换的,里面存放的是链表中节点的地址。

好的,下面我们要耐住性子,认真的学习咯(后文的故事如果听不太懂,放心你不是一个人,多看看不是故事的内容会更好);(下面是一个标准化的介绍)

// A simple C program to introduce 
// a linked list 
#include <stdio.h> 
#include <stdlib.h> 

//这个结构体就是我们要用到的节点了,
//既然c语言里的基本类型没有,我们就自己造一个。
struct Node { 
	int data; 
	struct Node* next; 
}; 

// Program to create a simple linkedlist with 3 nodes 
//编程去建立一个有三个节点的链表
int main() 
{ 
	//声明3个节点,注意了哈,这里是声明!!!
	//并没用得到那三个节点,就像我们拿到了三个空门牌,但是房间还没造出来呢
	struct Node* head = NULL; 
	struct Node* second = NULL; 
	struct Node* third = NULL; 

	// allocate 3 nodes in the heap
	//从堆中申请三个节点的内存(这会儿才得到房间了,并把房间号写在了门牌上) 
	head = (struct Node*)malloc(sizeof(struct Node)); 
	second = (struct Node*)malloc(sizeof(struct Node)); 
	third = (struct Node*)malloc(sizeof(struct Node)); 

	/* Three blocks have been allocated dynamically. 
	We have pointers to these three blocks as head, 
	second and third	 
	下面是灵魂的绘图(佩服佩服)来表示我们所申请的三个节点的储存状态,
	他们是不相连的哦
	head		        second	   	 third 
		|		         	 |		    	   | 
		|	          	 |	     		   |
	+---+-----+	 +----+----+	 +----+----+ 
	| # | # |	    | # | # |	    | # | # | 
	+---+-----+	 +----+----+	 +----+----+ 
	#代表了随机值,一个是数据,一个是指针,
	和我们的stuct node结构相同哦,
	但是他们现在还是随机值,因为我们没有给他们赋值。
	
	# represents any random value. 
	Data is random because we haven’t assigned 
	anything yet */

	head->data = 1; // assign data in first node 
	head->next = second; 
	// Link first node with the second node 
	// 我们给第一个节点赋值了,并且告诉了他下一个节点的位置

	/* data has been assigned to the data part of the first 
	block (block pointed by the head). And next 
	pointer of first block points to second. 
	So they both are linked. 
	//那个代码由于复制可能会造成错位,木得办法,撮合着看把哈哈。
		head		 second		 third 
			|			 |			 | 
			|			 |			 | 
	+---+---+	 +----+----+	 +-----+----+ 
	| 1 | o----->| # | # |	 | # | # | 
	+---+---+	 +----+----+	 +-----+----+	 
	*/

	// assign data to second node 
	second->data = 2; 

	// Link second node with the third node 
	second->next = third; 

	//同上,给第二个赋值,并且告诉他下一个在哪里
	/* data has been assigned to the data part of the second 
	block (block pointed by second). And next 
	pointer of the second block points to the third 
	block. So all three blocks are linked. 
	
	head		 second		 third 
		|			 |			 | 
		|			 |			 | 
	+---+---+	 +---+---+	 +----+----+ 
	| 1 | o----->| 2 | o-----> | # | # | 
	+---+---+	 +---+---+	 +----+----+	 */

	third->data = 3; // assign data to third node 
	third->next = NULL; 
	
	//给第三个(也是最后一个)赋值,并告诉他,你后面没人了。
	/* data has been assigned to data part of third 
	block (block pointed by third). And next pointer 
	of the third block is made NULL to indicate 
	that the linked list is terminated here. 

	We have the linked list ready. 

		head	 
			| 
			| 
		+---+---+	 +---+---+	 +----+------+ 
		| 1 | o----->| 2 | o-----> | 3 | NULL | 
		+---+---+	 +---+---+	 +----+------+	 

	Note that only head is sufficient to represent 
	the whole list. We can traverse the complete 
	list by following next pointers. */
	
	//接下来一定要试试效果么!
	struct Node* n = head;
	//新建立一个指针,让他去循环着跑
	//并且哈,它指向了第一个有数据的节点
	while (n != NULL) { 
        printf(" %d ", n->data); 
        n = n->next; //输出完后,他就有指向了下一个
    }
	return 0; 
}

这就完了?这就完了。

下面是一个简洁的代码:

// A simple C program for traversal of a linked list 
#include <stdio.h> 
#include <stdlib.h> 
  
struct Node { 
    int data; 
    struct Node* next; 
}; 
  
// This function prints contents of linked list starting from 
// the given node 
void printList(struct Node* n) 
{ 
    while (n != NULL) { 
        printf(" %d ", n->data); 
        n = n->next; 
    } 
} 
  
int main() 
{ 
    struct Node* head = NULL; 
    struct Node* second = NULL; 
    struct Node* third = NULL; 
  
    // allocate 3 nodes in the heap 
    head = (struct Node*)malloc(sizeof(struct Node)); 
    second = (struct Node*)malloc(sizeof(struct Node)); 
    third = (struct Node*)malloc(sizeof(struct Node)); 
  
    head->data = 1; // assign data in first node 
    head->next = second; // Link first node with second 
  
    second->data = 2; // assign data to second node 
    second->next = third; 
  
    third->data = 3; // assign data to third node 
    third->next = NULL; 
  
    printList(head); 
  
    return 0; 
}

当然,当我自己上手之后,生活还是狠狠地把我按在地上摩擦。。。

不妨我们模块化的写一下一个链表吧(写了很久来理解链表的优秀)

我先分开讲解每个模块的作用和意义,之后放在一起来观看,效果~~(可能)~~更佳。

1 必不可少的这个结构体呀!

你看,这个stuct Node *多长呀,如果我们使用的是typedef的话我们就可以将l作为一个数据类型的名字,然后把l当作int这样的来使用就好了(我们还是可以使用struct Node的)。

其次,看第二行,这个调用就很有意思了,声明了一个l数据类型的指针。

typedef struct Node
{
    int val;
    l* next;//等效 struct Node *next
}l;

2 我们要创建一个链表了。

我们在main函数里面声明了一个Head的指针,并且申请了一个内存空间,并把其地址放进了head。这个时候把他传过来。

或者说,我们要了一个房卡,并要了一个房间,这个房卡就是head,房卡对应的就是该房间。

第一步:

在creat函数里面:我们先是malloc 了一个l类型的空间,这个房间的地址赋值给了新声明的n指针上。【或者说我们向酒店索要了一间l类型的房间(房卡当然比房间要好拿到一些呀),同时把这个房间的门牌号输入到了门卡上。】

第二步:我们把指针n的内容(也就是新声明的节点的地址)赋值给head指针指向的next的位置【哈哈,有点懵吧,就是说,我们现在手里一共有两张房卡对吧,一张是head(head房卡可是能打开head房间的哦),另一张是n(n可是上面有房间号码的),然后我们打开head的房间,在里面的next区域放上n房卡的内容(就是刚刚申请的房间号/地址)(用这个例子可能好理解哈,指针总是很奇妙的)

第三步:将n赋值,并指向空【然后我们利用n房卡打开刚刚申请的房间哈,然后在内容区域放上内容,在下一个的区域放上空,说明后面没有房间了】

这就好了。想象一下我们拿着head房卡就可以了,先是到head的next区域找到下一个房间的位置,然后进去,就能看见第一个房间的内容区域还能看到下一个房间在哪里了,当然在此处我们没有下一个房间。

void creat(l *head){
    l *n = (l*)malloc(sizeof(l));
    if(n==NULL)cout<<"Error,malloc failed!";//这里做一下判断,如果内存不足,那么报错
		head->next = n;
    n->val = 6;
    n->next = NULL;   
}

3那我们一定要再来一个房间呀

第一步:传入一个头节点,和一个值【拿上head门卡和要往新房间里放的东西】

第二步:声明一个新的节点p,并使其和head指向相同。【再要一张门卡,复制一下head门卡的内容】这样做可以保证head不被破环。

第三步:开始找最后一个房间了,如果指向的房间中的next区域还有值,就说明后面还有房间,我们就把这个门卡p指向本来房间里的下一个房间位置,就是我们将p门卡本来打开时的是这个房间,我们使他打开的是下一个房间,(当然,他无法再打开这个房间了)然后继续搜索。直到发现只是最后一个房间。

第四步:我们新索要一个房间,原来最后一个空房间的next区域要存放上这个房间的位置了。然后我们把我们的东西也就是一个值放在新的房间,这个新房间就成了最后一个房间了,我们就需要把他的next变为NUll空,记住哦,这些步骤一步也不能缺少,否则就会酿成大错。。。(无限卡壳

void add(l*Head,int val){   
    l* p = Head;
    while(p->next)p=p->next;
    l*t = (l*)malloc(sizeof(l));
		p->next =t;
    t->val=val;
    t->next = NULL;
    }

4赶紧巡查一遍房间吧:

我们只用拿着head门卡就好,每次巡查的时候记得再要一个门卡自己的门卡保存着第一个房间的位置,可不能乱改。然后搜查完成一间房间之后,房卡n就成了下一个房间的房卡,继续搜查。哈哈

void disp(struct Node *head){
    struct Node *n = head->next;
    while(n){
        cout<<n->val<<" ";
        n = n->next;
    }
    cout<<endl;
}

总的代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef struct Node
{
    int val;
    struct Node* next;
}l;  

void disp(struct Node *head){
    struct Node *n = head->next;
    while(n){
        cout<<n->val<<" ";
        n = n->next;
    }
    cout<<endl;
}

void creat(l *head){
    l *n = (l*)malloc(sizeof(l));
    if(n==NULL)cout<<"Error,malloc failed!";
	head->next = n;
    n->val = 6;
    n->next = NULL;   
} 

void add(l*Head,int val){   
    l* p = Head;
    while(p->next)p=p->next;
    l*t = (l*)malloc(sizeof(l));
    p->next = t;
    t->val=val;
    t->next = NULL;

}
int main(){
    l *Head;
    Head = (l*)malloc(sizeof(l));
    
    creat(Head);
    add(Head,15);
    disp(Head);
    return 0;
}

只是写出一个链表就少了很多趣味,增改删查才是硬道理!

当然了,在开始阶段,我们还是只以单链表为例:

链表的增:

在这里插入图片描述

一、 在链表的末尾增加:(再重复的系统的讲一下哈)

这个方法我们在上面已经认识到了,具体的思路就是我们通过头节点找到最后一个节点,然后在她后面添加一个节点,链接上去就可以了。(我们手持一个head房卡,沿着房间不断地走下去,找到最后的房间,在新开一间房间,然后我们在原来的最后一个房间的next的空间里放上新开房间的位置就好了)

这里假设我们已经有了一个链表了。

void addLast(l*Head,int val){   
    l* p = Head;//设置临时的指针,用来指向不同的节点,实现在不影响head的情况下遍历等操作
    while(p->next)p=p->next;
    l*t = (l*)malloc(sizeof(l));
    p->next = t;
    t->val=val;
    t->next = NULL;
}

二、在链表的头部增加:

这个的做法似乎比上一个还要简单呢,毕竟我们不用去一一寻找到最后一个了。

既然我们需要添加一个值,那么新建一个节点(房间)是必不可少的了。然后将值放入,让其指向本来的第一个节点,在让head指向它就好。(不怕麻烦的话,也可以重新添加一个指针变量,作为中间值,来进行操作)

void addHead(l*Head,int val){
    l* t = (l*)malloc(sizeof(l));
    t->val = val;
    t->next = Head->next;
    Head ->next = t;
}

三、在链表的中间插入:

这个可以说是前两者的综合版本了。我们要先找到这个节点,然后对他进行插入。

这里就以给定一个值,作为目标值,在其后面添加一个节点吧。查找的方法是先检查一下它后面是否还有节点,如果有就判断他的值是否符合,如果不符合就使指针指向下一个节点。这样出来的结果只会有两种,一种是没有找到,也有可能是这个值就在最后一个节点里面。

这里做了两个版本的给定值寻找插入方法,一种是在其之前,另一种是在其后。(两者之间只需要一点点的代码改动就可以了,在不同的地方已经用//注释)

void insertBefore(l* Head,int target,int val){
    l* pre  = Head;//*
    l* p = Head;
    while(p->next){
        if(p->val==target)break;
        else {
             pre = p;//*
            p = p->next;
        }
    }
    if(p->next==NULL&&p->val!=target){
        cout<<"查找失败"<<endl;
        return;
    }
    l*t = (l*)malloc(sizeof(l));
    t->val = val;
    pre->next = t;//*
    t->next = p; //*
}

void insertAfter(l* Head,int target,int val){
    l* p = Head;
    while(p->next){
        if(p->val==target)break;
        else {
            p = p->next;
        }
    }
    if(p->next==NULL&&p->val!=target){
        cout<<"查找失败"<<endl;
        return;
    }
    l*t = (l*)malloc(sizeof(l));
    t->val = val;
     t->next = p->next;//*
     p->next = t;//*
}

对链表的删

???如果前面的掌握了的话,那么对链表的某个节点进行删除自然不是问题。就直接放代码了(思路和上面的insertBefore相同哦)(代码其实也是参考上面的

void del(l* Head,int target){//这里传入头节点和目标值就可以了
    l* pre  = Head;
    l* p = Head;
    while(p->next){
        if(p->val==target)break;
        else {
             pre = p;
            p = p->next;
        }
    }
    if(p->next==NULL&&p->val!=target){
        cout<<"查找失败"<<endl;
        return;
    }
		pre->next = p->next;//是的,将insertAfter代码的最后几行更改为这一行就好了。
		//(相当于将这个节点跳过去了)
	  free(p); //既然删除了这个节点,那么就把他释放掉,节约内存。
}

对链表的改:

???如果前面的掌握了的话,那么对链表的某个值进行更改自然不是问题。就直接放代码了(思路和上面的insertAfter相同哦)(代码其实也是参考上面的(人类的本质是复读机)

void change(l* Head,int target,int val){
    l* p = Head;
    while(p->next){
        if(p->val==target)break;
        else {
            p = p->next;
        }
    }
    if(p->next==NULL&&p->val!=target){
        cout<<"查找失败"<<endl;
        return;
    }
    p->val = val;//其实就是把insertAfter()最后的几行代码改为这个(2333)
   
}

对链表的查:

我们不是一直在查找么???(~~避免复读,~~就不再赘述)

看到这里,首先是一份敬佩,敬佩您能够静下心来一步一步的去尝试,去探索,去思考。确实文章篇幅很长,需要一定的耐心去思考,并且链表理解起来确实不是很困难,但是如果是刚开始,去上手操作,自然还是漏洞百出,bug重重。但是只要我们多尝试,多敲代码,缕清关系,明确指针自身的所在。那么,我们的各个方面都会有一定的成长。

谢谢阅读,本文中仍有许多的不足之处,还望交流指正。

对于后期,大家还可以去了解其他的链表形式,来加强对链表的使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章