链表的笔记

链表

线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。

为了表示每个数据元素 ai 与其直接后继数据元素 ai+1 之间的逻辑关系,对数据元素a_i来说,除了其本身的信息之外,还需要存储一个指示其直接后继的信息(直接后继的存储位置)。这两部分信息组成数据元素 ai 的存储映像,称为节点(node)

结点包括两个域:其中存储数据元素信息的称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。

单链表

只能从前往后走,无法反向遍历。

1.链表存储结构

typeddef int ElemType;

typedef struct node{
	ElemType data; //数据域
	struct node *next; //指针域(结构体本身类型的指针)
}Node;

2.单链表 - 初始化

Node* initList()
{
	Node* head = (Node*)malloc(sizeof(Node));//为头节点分配内存
	head->data = 0; //初始化头节点的数据域
	head->next = NULL; //初始化头节点的指针域
	return head;
}

int main()
{	
	Node* list = initList();
	return 1;
}

3.单链表 - 插入数据

(1)头插法

  • 在头节点后面插入数据
int insertHead(Node* L, ElemType e)//ElemType是前面给int起的别名
{
	Node* p = (Node*)malloc(sizeof(Node));//创建一个新的节点,存储要插入的数据
	p->data = e;//往新创建的节点的数据域中放入e
	p->next = L->next;//把传入的L节点指向的下一个节点(指针域)赋值给新创建的p节点
	L->next = p;//将头节点的next指针指向要插入的元素
}

int main()
{
	Node* list = initList();
	insertHead(list, 10);
	insertHead(list, 20);
}

必须先将要插入的节点的next指针指向头节点的下一个节点,然后再将头节点的next指针指向要插入的元素,若将这两步调换顺序,先将头节点的next指针指向要插入的元素后链表就在头节点那断开了,要插入的元素也无法链接到下一个元素。

(2)尾插法

  • 先获取尾节点地址
Node* getTail(Node* L)
{
	Node* p = L;
	while(p->next != NULL)
	{
		p = p->next;
	}
	return p;
}

尾插法代码的实现:

Node* insertTail(Node* tail, ElemType e)
{
	Node* p = (Node*)malloc(sizeof(Node));
	p->data = e;
	tail->next = p;
	p->next = NULL;
	return p;//新创建的p节点就是新的尾节点
}

(3)在指定位置插入

int insertNode(Node* L, int pos, ElemType e)
{
	Node *p = L;//先对传入来的节点备份
	int i = 0;
	while(i < pos - 1)//循环到想要插入位置的前一个位置
	{
		p = p->next;
		i++;
		if(p == NULL)
		{
			return 0;
		}
	}
	Node* q = (Node*)malloc(sizeof(Node));//q是要插入的新节点
	q->data = e;
	q->next = p->next;
	p->next = q;
	return 1;
}

4.单链表 - 遍历

void listNode(Node* L)
{
	Node* p = L->next;//第一个节点(头节点的下一个节点)
	while(p != NULL)
	{
		printf("%d\n",p->data);
		p = p->next;
	}
	printf("\n");
} 

5.单链表 - 删除节点

  • 第一步:找到要删除节点的前置节点 p
  • 第二步:用指针 q 记录要删除的节点
  • 第三步:通过改变 p 的后继节点实现删除
  • 第四步:释放删除节点的空间
int deleteNode(Node* L, int pos)
{
	Node* p = L;
	int i = 0;
	while(i < pos - 1)//找到要删除节点的前驱
	{
		p = p->next;
		i++;
		if(p == NULL)
		{
			return 0;
		}
	}
	if(p->next == NULL)
	{
		printf("要删除的位置错误!\n");
		return 0;
	}

	//核心实现步骤
	Node *q = p->next;//p是前驱,q指向要删除的节点
	p->next = q->next;//让要删除节点的前驱指向要删除节点的后继
	free(q);//释放掉删除节点的空间,因为这个节点是malloc出来的
	return 1;
}

6.获取链表长度

int listLength(Node* L)
{
	Node* p = L;
	int len = 0;//声明一个变量用来累加
	while(p != NULL)
	{
		len++;
		p = p->next;
	}
	return len;
} 

7.单链表 - 释放链表

把每一个节点的内存空间都释放掉,头节点保留。

  • 第一步:指针 p 指向头节点后的第一个节点
  • 第二步:判断指针 p 是否指向空节点
  • 第三步:如果 p 不为空,用指针 q 记录指针 p 的后继节点
  • 第四步:释放指针 p 指向的节点
  • 第五步:指针 p 和指针 q 指向同一个节点,循环上面的操作
void freeList(Node* L)
{
	Node* p = L->next;
	Node* q;
	
	while(p != NULL)
	{
		q = p->next;
		free(p);
		p = q;
	}
	L->next = NULL;
}

单链表的应用

1.快慢指针

  • 查找链表中倒数第k个位置上的元素(仅遍历一遍链表)

定义两个指针:快指针(fast)和慢指针(slow),让他们两个都指向第一个节点(头节点的下一个节点),先让快指针走 k 步,然后再让快慢指针同步向后走,知道快指针指向空值,那么此时慢指针指向的那个节点就是倒数第 k 个节点。代码实现如下:

int findNodeFS(Node* L, int k)
{
	Node* fast = L->next;
	Node* slow = L->next;
	for(int i = 0; i < k; i++)
	{
		fast = fast->next; //快指针先走k步
	}
	while(fast != NULL)
	{
		fast = fast->next;
		slow = slow->next;
	}
	printf("倒数第%d个节点值为:%d\n", k, slow->data);
}
  • 删除链表中间节点(偶数个时取后面的节点)

用快慢指针,慢指针指向头节点,快指针指向第一个节点,慢指针走一步,快指针走两步,当快指针的下一个节点为空时,慢指针的下一个节点就是要删除的中间节点。代码实现如下:

int delMiddleNode(Node* head)
{
	Node *fast = head->next;
	Node *slow = head;
	while(fast != NULL && fast->next != NULL)
	{
		fast = fast->next->next;
		slow = slow->next;
	}
	Node *q = slow->next;//指针q保存要删除的节点地址
	slow->next = q->next;//跳过要删除的节点,重新连接链表
	free(q);
	return 1;
}

2.反转链表

Node* reverseList(Node* head)
{
	Node *first = NULL;
	Node *second = head->next;//从第一个数据节点开始
	Node *third;

	while(second != NULL)
	{
		//核心操作
		third = second->next;//保存下一个节点
		second->next = first;//反转指针
		first = second;  //first后移
		second = third;  //second后移
	}
	//在原链表尾部创建头节点
	Node *hd = initList();//上文链表初始化实现的函数
	hd->next = first;
}

单向循环链表

循环链表(Circular Linked List)是另一种形式的链式存储结构。其特点是表中最后一个节点的指针域指向头节点,整个链表形成一个环。

当链表遍历时,判别当前指针p是否指向表尾结点的终止条件不同。在单链表中,判别条件为p!=NULL 或p->next!=NULL,而循环单链表的判别条件为p!=L 或p->next!=L。

判断链表是否有环
用快慢指针,快慢指针都指向头节点,快指针走两次,慢指针走一次,若快慢指针相遇则有环,反之则没有。代码实现如下:

int isCycle(Node *head)
{
	Node* fast = head;
	Node* slow = head;

	while(fast != NULL && fast->next != NULL)
	{
		fast = fast->next->next;
		slow = slow->next;
		if(fast == slow)
		{
			return 1;
		}
	}
	return 0;
}

链表有环入口在哪

  • 方法一:先确定环有多少个节点,让快慢指针在走一次至再次相遇,其中每走一步用count值进行记录(count++),然后将快慢指针再次同时指向头节点,让快指针先走count步,再让快慢指针同时走,相遇位置就是环的入口。代码实现如下:
Node* findBegin(Node *head)
{
	Node *fast = head;
	Node *slow = head;

	//先判断有没有环
	while(fast != NULL && fast->next != NULL)
	{
		fast = fast->next->next;
		slow = slow->next;
		if(fast == slow)
		{
			Node *p = fast;
			int count = 1;
			while(p->next != slow)
			{
				count++;
				p = p->next;
			}
			
			fast = head;
			slow = head;

			for(int i = 0; i < count; i++)
			{
				fast = fast->next;
			}
			while(fast != slow)
			{
				fast = fast->next;
				slow = slow->next;
			}
			return slow;
		}
	}
}
  • 方法二(执行效率高,但较难理解):快慢指针相遇,直接让慢指针回链表头,然后快慢指针同速前进,相遇点为入口。代码实现如下:
Node* findBegin(Node* head) {
    Node *slow = head;
    Node *fast = head;
    // 步骤1:判断是否有环
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) { // 相遇,说明有环
            // 步骤2:找环入口
            slow = head;
            while (slow != fast) {
                slow = slow->next;
                fast = fast->next;
            }
            return slow; // 相遇节点即入口
        }
    }
    return NULL; // 无环
}

原理

  • 链表头到环入口的距离为 a ;

  • 环入口到快慢指针第一次相遇点的距离为 b ;

  • 环的长度为 c 。

快慢指针第一次相遇时,快指针走了 a + b + kc ( k 是绕环圈数),慢指针走了 a + b ;又因快指针速度是慢指针的2倍,故 a + b + kc = 2(a + b) ,化简得 a = kc - b,a(链表头->环入口的距离) = kc(绕 k 圈的长度) - b(相遇点->入口的距离) 。

这意味着:从链表头走 a 步,与从相遇点走 (kc - b) 步(等价于绕环 k 圈后再走 c - b 步),会在环入口相遇。

双向链表

链式存储结构的节点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他节点。若要寻查结点的直接前驱、则必须从表头指针出发。换句话说,在单链表中,查找直接后继的执行时间为O(1),而查找直接前驱的执行时间为O(n)。

为克服单链表这种单向性的缺点,可利用双向链表(Double Linked List)。在双向链表的节点中有两个指针域,一个指向直接后继,另一个指向直接前驱。

typedef int ElemType;

typedef struct node{
	ElemType data;
	struct node *prev, *next;
}Node;

1.双向链表 - 初始化

Node* initList()
{
	Node *head = (Node*)malloc(sizeof(Node));
	head->data = 0;
	head->next = NULL;
	head->prev = NULL;
	return head;
}

2.双向链表 - 头插法

先将要插入的元素的前后指针都绑定到链表中,然后再将原链表中的前后指针改连到新节点上。

int insertHead(Node* L, ElemType e)
{
	Node *p = (Node*)malloc(sizeof(Node));
	p->data = e;
	p->prev = L;
	p->next = L->next;
	if(L->next != NULL)
	{
		L->next->prev = p;//L->next获得头节点的下一个节点,->prev获得第一个节点的前驱指针
	}
	L->next = p;
	return 1;
}

3.双向链表 - 尾插法

先让新节点的前驱指向尾节点,再让尾节点的后继指向新节点,最后将新节点后继赋值为空。

Node* insertTail(Node *tail, ElemType e)
{
	Node *p = (Node*)malloc(sizeof(Node));
	p->data = e;
	p->prev = tail;
	tail->next = p;
	p->next = NULL;
	return p;	
}

4.双向链表 - 在指定位置插入数据

int insertNode(Node* L, int pos, ElemType e)
{
	Node *p = L;
	int i = 0;
	while(i < pos - 1)
	{
		p = p->next;
		i++;
		if(p == NULL)
		{
			return 0;
		}
	}
	
	Node* q = (Node*)malloc(sizeof(Node));
	q->data = e;
	q->prev = p;
	q->next = p->next;
	p->next->prev = q;
	p->next = q;
	return 1;
}

5.双向链表 - 删除节点

  • 第一步:找到要删除节点的前置节点 p
  • 第二步:用指针 q 记录要删除的节点
  • 第三步:通过改变 p 的后继节点以及要删除节点的下一个节点的前驱实现删除
  • 第四步:释放删除节点的空间
int deleteNode(Node* L, int pos)
{
	Node* p = L;
	int i = 0;
	while(i < pos - 1)
	{
		p = p->next;
		i++;
		if(p == NULL)
		{
			return 0;
		}
	}
	if(p->next == NULL)
	{
		printf("要删除的位置错误!\n");
		return 0;
	}

	Node *q = p->next;
	p->next = q->next;
	q->next->prev = p;
	free(q);
	return 1;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值