想入门数据结构,却总是拜倒在链表的石榴裙下?

大家好,我是melo,一名大二上软件工程在读生,经历了一年的摸滚,现在已经在工作室里边准备开发后台项目啦
不过这篇文章呢,还是想跟大家聊一聊数据结构与算法,学校也是大二上才开设了数据结构这门课,希望可以一边学习数据结构一边积累后台项目开发经验

写在前边

相信很多小猿人在初入数据结构的时候,或者说是在学习c语言的后期时分,总会遇到一个馋(缠)人的绕来绕去的家伙--就是我们今天要讲的链表。为什么说链表缠人呢,链表的分类,题型的多样性,链表的用途等等都是很多的,限于篇幅本篇着重来讨论一下学习链表以及做题时的思路概览,注意点以及有哪些小技巧
另外,本博客题解一般会使用c(学校课内要求)和java(个人后台方向需要熟练应用)两种语言

ps:本博客旨在提供一些学习和做题时的小技巧,推荐搭配数据结构算法书籍一同学习和补充知识点
入门的话比较推荐的有《啊哈!算法》,《大话数据结构》,这两本书都偏向图文式教学,也有很多可可爱爱的插画
image.png

小哈确实好可爱啊哈哈哈

  • 关于链表的定义,优点相信你们也都在书中看了蛮多了,这里就不多加阐述,我们直接来看形形色色的各种链表,看他们的区别和实现

链表的分类

我们先来看看最简单的单向链表

  • 链表是有序的列表,但是它在内存中是这样存储的

image.png
1)链表是以节点的方式来存储,是链式存储
2)每个节点包含data域,next域:指向下一个节点.
3)如图:发现链表的各个节点不一定是连续存储.
4)链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定

带头结点逻辑结构示意图如下

head中不存储实际的数据data,只是用next来指向真正带有数据的第一个节点

image.png

不带头结点

第一个节点即存储有实际数据data和next的实际节点

何时要头结点何时不需要呢

  • 有无头结点的区别在于,链表的第一个节点是否存储值。那我们可以先大概下个定义:如果说我们的操作会影响到第一个节点的(比如说删除链表中的某个值),那是不是我们尽量就用带头结点的会好一点

例如:删除结点时,如果我们不带头结点的话,如果删除的是第一个实际节点,那我们还需要去更改头结点,让头结点指向第二个实际节点(因为第一个实际节点已经被删除了,自然就不是头结点了)
而带有头结点的话,恰好就能解决这种需要特判的情况

这个区别,还需要我们慢慢去体会,在下文讲到链表删除的时候自然而然就会更加深刻体会到了,这里只是先点一下,而到了后边环形链表的时候,更多的是没带头结点的版本

Typedef(偷懒神器)

  • PNODE就等价于 struct Node* 了!

image.png
image.png

LinkList就直接等价于struct LNode*,可以省去很多书写,下文中也会体现到

创建链表

思路概览

  • 我们需要两个结点,一个指向当前节点,一个指向我们新创建的节点,每次创建新节点后,让当前节点指向新节点,并移动当前节点到新节点上

记得最后跳出循环的时候,要让当前节点指向null来结束链表!

流程

链表节点定义(typedef妙用,后续可以省去struct)

#include<cstdio>
#include <malloc.h>

typedef struct ListNode {
	int val;
	struct ListNode* next;
};

注意c语言需要修改头文件中的cstido为stdio.h

注意让当前pNow=head

可以省去特判i=0

//根据读取的元素创建链表
ListNode* creatLinkedList() {
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	ListNode* pNow = head;
	int n,temp;
	scanf("%d", &n);
	for (int i = 0; i < n; i++) {
		ListNode* pNew = (ListNode*)malloc(sizeof(ListNode));
		scanf("%d", &(pNew->val));
		//若是第一个
		/*if (i == 0) {
			head->next = pNew;
		}*/
		//将当前节点指向新节点,并移动当前节点到新节点上
		pNow->next = pNew;
		pNow = pNew;
	}
	//跳出循环后,使得当前节点指向null终止链表
	pNow->next = NULL;
	return head;
}

特判第一个多一步 head->next=pNew

其实可以不用,在不带头结点的链表创建中,才需要去特判头结点

记住最后跳出循环让pNow->next=null

遍历链表

  • 注意让p=head->next,然后遍历p(p=p->next)判断 p !=null 即可
//遍历链表
void traverseLinkedList(ListNode* head) {
	for (ListNode* p = head->next; p != NULL; p=p->next) {
		printf("%d ", p->val);
	}
}

链表插入

在指定位置插入

image.png

思路概览

  • 首先我们第一感觉就是邀先移动到指定位置的前一位p,然后去新开辟一个节点pNew,让p指向pNew,pNew指向原来p的下一节点

问题

  • 如果我们原原本本按照思路来,有没有发现,其实让pNew指向原来p的下一节点,但是此时p的下一节点是什么呢,其实已经是pNew了

所以我们应该先用另一个指针,存储一下原来p都下一个节点
或者说注意一下顺序,先让pNew的next=p的next
然后再让p都next=pNew

注意

**校验合法性

  • 用while 和if 两个条件刚好相反 可以省去获取链表长度的操作
//链表插入(三种情况都综合成一种)
bool insertLinkedList(ListNode* head, int position, int data) {
	int i = 0;
	ListNode* p = head;
	ListNode* pNew = (ListNode*)malloc(sizeof(ListNode));
	//移动p直到目标位置的前一位,条件是p不为null
	while (i < position - 1 && p!=NULL) {
		i++;
		p = p->next;
	}
	//若移动不到,返回false
	if (i != position - 1 || p==NULL) {
		return false;
	}
	pNew->val = data;
	//注意顺序
	//先让新节点指向上一个节点的下一节点
	//然后再让上一个节点指向新节点
	pNew->next = p->next;
	p->next = pNew;
	return true;
}

链表删除

(力扣)203移除链表元素(删除所有满足指定值的节点)

思路概览

  • 我们需要定义两个指针,一个last记录前一个节点,一个记录当前节点now,怎么删除呢?让last(图中的p)->next=now(图中的q)->next就可以了

image.png

***递归还未看

https://leetcode-cn.com/problems/remove-linked-list-elements/

特判头指针法

struct ListNode* removeElements(struct ListNode* head, int val){
    //前指针
    struct ListNode* last=head;
    //当前指针
    struct ListNode* now=head;
    //now肯定要移动,而last就不一定要移动了!!!(此处注意)
    for(;now!=NULL;now=now->next){
        if(now->val==val){
            //特判删除头节点
            if(now==head){
                head=now->next;
            }
            //一般情况
            else{
                last->next=now->next;
            }
        }
        //不删,则移动last,若删了就不用移动!!!
        else{
            last=now;
        }
    }
    return head;
}

注意last需不需要移动!!!,且last默认先给头节点

虚拟头结点法(注意最后return)

struct ListNode_ dummyHead= (struct ListNode_)malloc(sizeof(struct ListNode));

struct ListNode* removeElements(struct ListNode* head, int val){
    //创建虚拟头节点,并指向第一个节点
struct ListNode* dummyHead= (struct ListNode*)malloc(sizeof(struct ListNode));
    dummyHead->next=head;
    //前指针
    struct ListNode* last=dummyHead;
    //当前指针
    struct ListNode* now=head;
    //now肯定要移动,而last就不一定要移动了!!!(此处注意)
    for(;now!=NULL;now=now->next){
        if(now->val==val){
            //特判删除头节点
            last->next=now->next;
        }
        //不删,则移动last,若删了就不用移动!!!
        else{
            last=now;
        }
    }
    return dummyHead->next;
}

(最优)while+虚拟头

delete temp是c++中的,此处java会自动回收无需释放空间

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //创建虚拟头结点
        ListNode dummyHead = new ListNode(0);
        //让虚拟头结点指向给定的head结点
        dummyHead.next = head;
        //定义一个临时结点来遍历,默认从虚拟头结点开始
        ListNode temp = dummyHead;
        while (temp.next != null) {
            if (temp.next.val == val) {
                //找到了则删除
                temp.next = temp.next.next;
            } else {
                //找不到继续移动
                temp = temp.next;
            }
        }
        return dummyHead.next;
    }
}

**移除链表元素(指定位置)

题目

image.png

题解

#include "allinclude.h"  //DO NOT edit this line
Status Delete_L(LinkList L, int i, ElemType &e) 
{   // Add your code here
  LNode* p = L;
  int j=0;
  //注意跟新增一个节点不一样,此处得p->next!=NULL
  while(j<i-1&&p->next!=NULL){
    p=p->next;
    j++;
  }
  //若参数不合法
  if(j!=i-1||p->next==NULL){
    return ERROR;
  }
  e= (p->next)->data;
  //LNode* temp = p->next;
  p->next = p->next->next;
  //free(temp);
  return OK;

}

**注意

  • 跟新增一个节点不一样,此处得p->next!=NULL,而不是p!=NULL

若是p!=NULL的话,下边我们又要保存p->next,可能就非法访问了!

*从第i元素起的所有元素从链表移除

题目

image.png

还记得上文的typedef吗,此处定义了一个LinkList就等价于LNode*
相当于Typedef struct LNode LinkList*

题解

#include "allinclude.h"  //DO NOT edit this line
Status Split_L(LinkList L, LinkList &Li, int i)
{   // Add your code here
  int cnt=0;
  LNode* p = L ;
  LinkList temp;
  //移动到前一位  
  while(cnt<i-1 && p->next!=NULL){
    p=p->next;
    cnt++;
  }
  //校验参数合法性  
  if(cnt!=i-1||p->next==NULL){
    Li=NULL;
    return ERROR;
  }
  temp=(LinkList)malloc(sizeof(LNode));  
  if(temp==NULL) return ERROR;
  Li=temp;  
  Li->next=p->next;
  //销毁i元素后  
  p->next=NULL;   
  return OK;
}

链表反转

(力扣)206反转链表

https://leetcode-cn.com/problems/reverse-linked-list/

image.png

思路概览

  • 让后一个指向前一个,同时要记得要用另外一个指针保存下一个节点位置用于正常遍历,总共需要三个指针

官方题解(迭代+三指针)

c语言

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* prev = NULL;
	struct ListNode* curr = head;
	while (curr) {
		//先记录curr的下一位,后续才能修改curr的下一位(总结就是要修改哪个值,就先保存下来)
		struct ListNode* next = curr->next;
		curr->next = prev;
		prev = curr;
		curr = next;
	}
	return prev;
}

java版

区别就在于java没有指针而是引用,直接ListNode就好,没有那个*(写法方便一些)

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
}

Tips

要改变什么值,就先用一个临时值来存储

链表反转中,要改变next,又怕影响正常的遍历,所以先用一个临时指针来存储就好了,这样就还能正常的遍历下去

合并两个有序链表

(力扣)21合并两个有序链表

https://leetcode-cn.com/problems/merge-two-sorted-lists/

image.png

思路

  • 同时遍历两个链表l1和l2,直到有一个为null
  • 每次遍历时,判断是哪个比较小,然后加到新链表中,移动l1指针

直接if单独比较版本

一次只能一个,但是就不用嵌套while(while还要再次判断是不是空)

struct ListNode* mergeTwoLists2(struct ListNode* l1, struct ListNode* l2) {
	struct ListNode* drummyHead = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* Node = drummyHead;
	while (l1 != NULL && l2 != NULL) {
		if( l1->val <= l2->val) {
			Node->next = l1;
			l1 = l1->next;
		}
		else{
			Node->next = l2;
			l2 = l2->next;
		}
		Node = Node->next;
	}
	//最后出循环必有一个为null(而且不会同时为null)则让非空的剩余部分连上去即可
	Node->next = l1 == NULL ? l2 : l1;
	/*ListNode* p = drummyHead->next;
	while (p) {
		printf("%d", p->val);
		p = p->next;
	}*/
	return drummyHead->next;
}

while中嵌套while版本

注意

内部while可能会破坏外部大while的条件,需要再次判断

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
	struct ListNode* drummyHead = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* Node = drummyHead;
	while (l1 != NULL && l2 != NULL) {
		while (l1 != NULL && l2 != NULL && l1->val <= l2->val) {
			Node->next = l1;
			Node = Node->next;
			l1 = l1->next;
		}
		//可能l1已经为NULL了,所以也还得判断l1
		while (l2 != NULL && l1 != NULL && l2->val <= l1->val) {
			Node->next = l2;
			Node = Node->next;
			l2 = l2->next;
		}
	}
	//最后出循环必有一个为null(而且不会同时为null)则让非空的剩余部分连上去即可
	Node->next = l1 == NULL ? l2 : l1;
	/*ListNode* p = drummyHead->next;
	while (p) {
		printf("%d", p->val);
		p = p->next;
	}*/
	return drummyHead->next;
}

总结

Tip

巧用typedef偷懒神器

有时候题目给的是不带头结点的,尽量自己去构造一个虚拟头指针,可以省去一些特判的操作

要改变什么值,就先用一个临时值来存储

链表反转中,要改变next,又怕影响正常的遍历,所以先用一个临时指针来存储就好了,这样就还能正常的遍历下去

最后

  • 有关其他链表知识,环形链表,约瑟夫等经典问题,melo最近比较忙,忙于学习多线程知识接管一下项目,等到以后学校课程(小声bb学校刚讲到创建和遍历链表)跟进了再后续补充,后续也会更新一些多线程的知识以及设计模式等内容,希望大家多多体谅和包涵

最近学校讲到栈和队列了,过段时间应该也会继续更进完善栈和队列相关的博客

posted @ 2021-09-09 10:47  Melo~  阅读(192)  评论(0编辑  收藏  举报