三、链表(实践)

絮絮叨叨

如何轻松写链表的代码?

  • 有决心并付出精力
  • 理解指针或引用的含义
    • 将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
  • 警惕指针丢失和内存泄漏
  • 利用哨兵(头结点)简化实现难度
  • 重点留意边界条件处理
    • 如果链表为空时,代码是否能正常工作?
    • 如果链表只包含一个结点时,代码是否能正常工作?
    • 如果链表只包含两个结点时,代码是否能正常工作?
    • 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
  • 举例画图,辅助思考

一、数据结构

1、单向链表

  • 结点包括:数据域 + 指针域
    • 数据域:存储数据元素的值
    • 指针域(链域):存储下一个结点地址或者指向其后继结点的指针
// 定义结点 Node
typedef struct Node{
	ElemType data;
	struct Node * next;
} Node;
// 定义指向结点 Node 类型对象的指针 LinkList
typedef struct Node *LinkList; 

2、双向链表

  • 结点包括:数据域 + 左指针域(prev) + 右指针域(next)
struct DNode{
	int data;
	DNode * prev;
	DNode * next;
}

二、基本操作实例

1、单链表的读取

(1)获取链表第 i 个数据结点的算法思路:

  • 声明一个指针 p 指向链表的第一个结点,初始化 j 从 1 开始;
  • 当 j < i 时,遍历链表,p 不断指向下一个结点, j++;
  • 若到链表末尾 p 为空, 则说明第 i 个元素不存在;
  • 否则查找成功,返回结点 p 的数据。

(2)实现

由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此不方便用for循环来控制循环。==》while循环

# define OK 1
# define ERROR 0
# define TRUE 1
# define FALSE 0
typdef int Status; //Status是函数的类型,其值为函数结果状态码,eg:OK等
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果: 用e返回L中第i个数据元素的值*/
Status GetElem( Node *L, int i, Elemtype *e){	
	int j;
	LinkList p; // 声明指针p
	p = L->next; //让p指向链表L的第一个节点
	j = 1;
	while(p && j<i) //当p不为空 或 j 小于i时,继续循环
	{
		p = p->next;
		++j;
	}
	if(!p || j>i )
		return ERROR;
	*e = p->data;
	return OK;
}

2、插入结点(单向链表)

s->next = p->next;
p->next = s

在这里插入图片描述

(1)第 i 个数据插入结点的算法思路:

  • 声明一个指针p指向链表的第一个结点,初始化j=1;
  • 当 j < i 时,遍历链表,让指针 p 向后移动,不断指向下一结点,++j;
  • 若到链表末尾 p 为空,说明第 i 个元素不存在;
  • 否则查找成功,在系统中生成一个空结点 s;
  • 将数据元素 e 赋值给 s->data;
  • 单链表的插入标准语句:s->next = p->next; p->next = s
  • 返回成功

(2)实现

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(LinkList *L, int i, ElemType e)
{
	int j;
	LinkList p,s;
	p = *L;
	j = 1;
	while(p && j<i)  //寻找第i个结点
	{
		p = p->next;
		++j;
	}
	if(!p || j>i ) // 第i个结点不存在
		return ERROR;
	s = (LinkList)malloc(sizeof(Node)); //生成新的结点
	s->data = e;
	s->next = p->next; //将p的后继结点赋值给s的后继
	p->next = s;       //将s赋值给p的后继
	return OK;
}

3、删除结点(单向链表)

p->next = p->next->next
用q取代p->next的话,上面等价于:

q = p->next;  
p->next=q->next

在这里插入图片描述

(1)第 i 个数据删除结点的算法思路:

  • 声明一个指针p指向链表的第一个结点,初始化 j 从1开始;
  • 当 j<i 时,遍历链表,让指针p向后移动,不断,指向下一结点,++j;
  • 若到链表末尾p为空,说明第i个结点不存在;
  • 否则查找成功,将欲删除的结点p->next赋值给q;
  • 单链表的删除标准语句:p->next = q->next;
  • 将q结点中的数据赋值给e,作为函数的返回值
  • 释放q结点
  • 返回成功

(2)实现

#include<stdio.h>

Status ListDelete(LinkList *L, int i, ElemType *e)
{
	int j;
	LinkList p,q;
	p = *L;
	j = 1;
	while(p->next && j<i){   //遍历查找第i个结点
		p = p->next;
		++j;
	}
	if(!(p->next) || j>i)
		return ERROR;
	q = p->next;
	p->next = q->next;
	*e = q->data;
	free(q);
	return OK;
}

4、单链表的整表创建

单链表的整表创建过程就是一个动态生成链表的过程。由“空表”的初始状态,依次建立各元素结点,并逐个插入链表。

(1)算法思路(头插法):

  • 声明一结点 p 和 计数器变量 i;
  • 初始化一空链表 L;
  • 让 L 的头结点的指针指向NULL,即建立一个带头结点的单链表;
  • 循环:
    • 生成一个新结点赋值给 p;
    • 随机生成一个数组赋值给 p 的数据域 p->data;
    • 将 p 插入到头结点与前一新结点之间。

(2)实现

头插法

/* 随机产生n个元素的值,建立带头结点的单链表L */ 
void CreateListHead(LinkList *L, int n)
{	
	LinkList p;
	int i;
	srand(time(0));     //初始化随机种子
	*L = (LinkList)malloc(sizeof(Node));
	(*L)->next = NULL;  //建立一个带头结点的单链表
	for(i = 0; i < n; i++)
	{
		p = (LinkList)malloc(sizeof(Node)); //生成新结点
		p->data = rand()%100 + 1;
		p->next = (*L)->next;
		(*L)->next = p;
	}
}

尾插法

/* 随机产生n个元素的值,建立带头结点的单链表L */ 
void CreateListHead(LinkList *L, int n)
{ 
	LinkList p, r;
	int i;
 	srand(time(0));     //初始化随机种子
 	*L = (LinkList)malloc(sizeof(Node));
 	r = *L;             // *r 指向尾部的结点
 	for(i = 0; i < n; i++)
 	{
  		p = (LinkList)malloc(sizeof(Node)); //生成新结点
  		p->data = rand()%100 + 1;
  		r->next = p;
 		r = p;
 	}
 	r->next=NULL;
}

5、单链表的整表删除

(1)算法思路

  • 声明结点结点 p 和 q;
  • 将第一个结点赋值给 p;
  • 循环:
    • 将下一结点赋值给 q;
    • 释放 p;
    • 将 q 赋值给 p;

(2)实现

/*初始条件:顺序线性表L已存在,操作结果:将L充值为空表*/
Status CLearList(LinkList *L)
{	
	LinkList p,q;
	p = (*L)->next;
	while(p)
	{
		q = p->next;
		free(p);
		p = q;
	}
	(*L)->next = NULL;
	return OK;
}

三、常见操作

1、单链表反转

法一:反向遍历链表就类似于事先遍历的节点后输出,即“先进后出”,那么可以将链表遍历存放于栈中,其后遍历栈依次弹出栈节点,达到反向遍历效果。

//1.stack
void printLinkedListReversinglyByStack(Node *head){
    stack<Node* > nodesStack;
    Node* pNode = head;
    //遍历链表
    while (pNode != NULL) {
        nodesStack.push(pNode);
        pNode = pNode->next;
    }
    while (!nodesStack.empty()) {
        pNode=nodesStack.top();
        printf("%d\t", pNode->value);
        nodesStack.pop();
    }
}
//2.递归
void printLinkedListReversinglyRecursively(Node *head){
    if (head!=NULL) {
        if (head->next!=NULL) {
            printLinkedListReversinglyRecursively(head->next);
        }
        printf("%d\t", head->value);
    }
}

2、链表中环的检查,获取连接点,计算环的长度

判断链表是否有环路,获取连接点,计算环的长度
此题很有意思,具体详细请参考:http://www.cnblogs.com/xudong-bupt/p/3667729.html

判断是否含有环:slow和fast,slow指针每次走一步,fast指针每次走两步,若是链表有环,fast必能追上slow(相撞),若fast走到NULL,则不含有环。

//判断是否含有环
bool containLoop(Node* head){
    if (head==NULL) {
        return false;
    }
    Node* slow = head;
    Node* fast = head;
    while (slow!=fast&&fast->next!=NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    if (fast==NULL) {
        return false;
    }
    return true;
}

判断环的长度:在相撞点处,slow和fast继续走,当再次相撞时,slow走了length步,fast走了2*length步,length即为环得长度。

//获得环的长度
int getLoopLength(Node* head){
    if (head==NULL) {
        return 0;
    }
    Node* slow = head;
    Node* fast = head;
    while (slow!=fast&&fast->next!=NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    if (fast==NULL) {
        return 0;
    }
    //slow和fast首次相遇后,slow和fast继续走
    //再次相遇时,即slow走了一圈,fast走了两圈
    int length = 0;
    while (slow!=fast) {
        length++;
        slow = slow->next;
        fast = fast->next->next;
    }
    return length;
}

环得连接点:slow和fast第一次碰撞点到环的连接点的距离=头指针到环的连接点的距离,此式可以证明,详见上面链接。

//获得环的连接点
Node* getJoinpoit(Node* head){
    if (head==NULL) {
        return NULL;
    }
    Node* slow = head;
    Node* fast = head;
    while (slow!=fast&&fast->next!=NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    if (fast==NULL) {
        return NULL;
    }
    Node* fromhead = head;
    Node* fromcrashpoint = slow;

    while (fromcrashpoint!=fromhead) {
        fromhead = fromhead->next;
        fromcrashpoint = fromcrashpoint->next;
    }
    return fromhead;
}

3、找出中间节点

用slow和fast指针标记,slow每次走一步,fast每次走两步,当fast到尾节点时,slow就相当于总长度的一半,即在中间节点。

//找出中间节点
Node* findMidNode(Node* head){
    Node* slow = head;
    Node* fast = head;
    while (fast->next != 0&&fast->next->next!=0) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

4、找出倒数第k个节点

用slow和fast指针标记,fast指针事先走k步,然后slow和fast同时走,当fast到达末节点时,slow在fast的前k个节点,即为倒数第k个节点。

//找出倒数第k个节点
Node* findKNode(Node* head,int k){
    Node *temp1 = head;
    Node *temp2 = head;
    while (k-->0) {
        if(temp2 == NULL){
            return NULL;
        }
        temp2 =temp2->next;
    }
    while (temp2->next != NULL&&temp2->next->next!=NULL) {
        temp1 = temp1->next;
        temp2 = temp2->next->next;
    }
    return temp1;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值