数据结构入门——链表

一、了解链表

1.单链表与双链表

1).链表可以抽象成一列运输火车,火车的每节车厢通过一定的方式进行连接,每节车厢里面都可以装载不同的物品。查找数据就是遍历链表,在火车里面从车头走到车尾的过程

2).链表分为单向链表和双向链表,顾名思义:

单链表双链表
一个节点只有一个指向后继节点的指针一个节点有一个指向前驱节点的指针
和一个指向后继节点的指针
只有一个头结点,若丢失则整个链表丢失可以从任意节点开始 从前往后/从后往前 遍历
尾结点指针指向 Null头结点和尾结点都有个指向 Null 的指针

单链表:
单链表
双链表:
双链表

2.链表学习流程分享

大概总结了一下链表的学习思路,若有更好的建议欢迎提出~
链表结构

二、链表基本知识

1. 单链表的简单实现

这里写的简单实现是基于 单个[int] 类型,比较好进行测试,可以尝试使用其他类型或者多个其他类型的元素。

定义一个单链表节点类

节点类内部就是需要储存的元素(可以是其他类型的元素,或多个元素),一个指向下一个节点的指针引用

class Node {
	// 每个节点存储的值
	int val;
	// 指向下一个节点的引用
	Node next;
	
	Node() {}
	
	Node(int val) {
		this.val = val;
	}
	Node(int val, Node next) {
		this.val = val;
		this.next = next;
	}
}

Node节点抽象:
一个node节点

定义单链表类

1.统计链表节点个数 (int size;, 作为索引) 和定义一个链表头节点 (Node head;)

2.拥有一个头结点就等于拥有了整个单链表,所以必须保证头结点使用一个引用来定义,这样在遍历的时候才不会丢失链表。

3.所有增删改查的方法都写在这个类中

public class myLinkedList {
	// 链表有效节点个数,需要添加或删除的时候自己统计
	private int size;
	// 链表的头结点,不能丢失
	private Node head;
}
节点的插入(增)

有两种方式,头插法和尾插法;

头插法

示例:从头部插入节点

public void addFirstHead(int val) {
	// 创建节点就是创建一个 Node 对象并传入 输入的 val 值
	Node node = new Node(val);
	// 首先判断头结点是否为空,若为空则此节点就是第一节点
	if (head == null) {
		head = node;
	} else {
		// 此时存在前驱节点,使用头插法从头部进行插入
		// 将新节点的引用指向原链表的头部
		node.next = head;
		// 再让头节点变成新节点
		head = node;
	}
	// 统计节点个数
	size ++;
}

头插法

尾插法

示例:从尾部插入节点

public void addLastNode(int val) {
	Node node = new Node(val);
	// 一样首先判空
	if (head == null) {
		head = node;
	} else if (head.next == null){
		// 若头结点存在,且头结点指向空,此时只有一个节点,从尾部插入新节点
		head.next = node;
	} else {
		// 若头结点后边跟着很多节点,则需要遍历到尾部后再插入
		Node pre = head.next;
		while (pre.next != null) {
			pre = pre.next;
		}
		// 此时走到了链表最后一个节点处,添加新节点即可
		pre.next = node;
	}
	size ++;
}

尾插法

一般来说没有顺序要求优先使用头插法,可以节省遍历的时间复杂度,需要考虑的条件不多,写起来也比尾插法简单很多。

插入中间节点

插入中间节点是头插法和尾插法相结合,其实这个方法完全可以直接替代尾插法

示例:在 index 位置插入 val 值的节点
思路:先遍历到 index 位置的前一位再进行插入

public void addIndex(int index, int val) {
	// 首先判断索引是否越界
	if (index < 0 || index > size) {
		// 若越界则报错
		System.err.println("添加失败,该索引非法!");
	}
	// 分两种情况,索引为0,和索引不为0
	if (index == 0) {
		// 在头部插入,直接调用头插法
		addFirstNode(val);
	} else {
		// 和尾插法思路类似,先遍历到需要插入的位置
		// 需要插入的节点是在 index 位置,这个插入操作必须在 index 的前一位开始,
		// 所以 index - 1
		for (int i = 0; i < index - 1; i++) {
			pre = pre.next;
		}
		// 找到前驱节点后就可以进行下一步插入操作
		// 在新建节点的时候传入 val 和 prev.next (值 和 prev 指向的下一个节点)
		// prev指向的下一个节点后面的链表就不会丢失
		Node node = new Node(val,pre.next);
		// 拼接插入新节点后的链表
		pre.next = node;
	}
}

pre 停留在 2 的位置再做后继插入操作
在这里插入图片描述

简化前的插入方式

// 新建一个值为 val 的节点
Node node = new Node(val);
// 新节点的 next 引用指向 prev 的下一个节点拼接上原链表
node.next = prev.next;
// prev的 next 引用指向新节点 node,完成整个拼接过程
prev.next = node;

中间节点插入分四步:

  • 新建节点
  • 新节点引用先指向需要插入位置的后继节点
  • 断开原先链表中该位置前驱节点与后继节点的连接
  • 前驱节点引用指向新节点

中间节点的插入
尾插可以调用中间插入节点的方法直接改写成

public void addLast(int val) {
	addIndex(size, val);
}
修改节点 (改)

修改 index 节点的值,返回修改前的值
和插入中间节点的思路差不多,先遍历,不同的是 index 是直达该节点

public int set(int index, int newVal) {
	// 先判段是否越界
	if (index < 0 || index >= size) {
		System.err.println("该引用对应位置不存在,修改错误");
		return -1;
	} 
	Node node = head;
	for (int i = 0; i < index; i++) {
		node = node.next;
	}
	// 暂存一下需要返回的原链表值
	int oldVal = node.val;
	// 修改原链表值为新值
	x.val = newVal;
	return oldVal;
}
查找节点 (查)

输入的值全部为正整数的情况

1.输入一个值查找该节点的位置,存在返回该节点索引,若不存在返回 -1

public index getVal(int val) {
	int index = 0;
	// 使用 foreach 循环遍历链表
	for (Node x : head) {
		if (x.val == val) {
			return index;
		}
		index ++;
	}
	// 否则返回 -1
	return -1;
}

2.输入一个索引查找对应的节点

public int get(int index) {
	// 有索引值必须判断是否越界
	if (index < 0 || index >= size) {
		System.err.println("该索引对应位置不存在,获取值失败");
		return -1;
	}
	Node node = head;
	for (int i = 0; i < index; i++) {
		node = node.next;
	}
	// 找到后返回该节点的值
	return node.val;
}

3.输入一个值查找该节点是否存在
直接调用第一个查找方法即可,若结果不为 -1 即找到

public boolean contains(int val) {
	return getVal(val) != -1;
}
删除节点 (删)

1.删除索引值为 index 的节点,思路与在插入中间节点类似
分情况讨论:头结点是待删除的节点 与 中间节点是待删除的节点

public int remove(int index) {
	// 判断 index 是否越界
	if (index < 0 || index >= size) {
		System.err.println("输入的索引不存在,删除失败");
		return -1;
	}
	// 头结点是待删除的节点
	if (index == 0) {
		// 为避免待删除节点还挂在链表上的情况,需要将该节点的引用置为空
		Node tmp = head;
		head = head.next;
		tmp.next = null;
	} else {
		// 头结点不是待删除的节点,使一个指针遍历到待删除节点位置标记
		Node pre = head;
		// pre 在待删除节点的前一个节点停下
		for (int i = 0; i < index - 1; i++) {
			pre = pre.next;
		}
		Node node = prev.next; // 临时储存待删除节点
		prev.next = node.next; // 连接待删除节点的下一个节点
		node.next = null; // node 就是待删除节点,引用置空
		size --; // 索引 - 1
		return node.val; // 返回已删除节点的值
	}
}

删除节点分三步

  • 暂存待删除节点 node
  • 前驱节点 pre 引用指向待删除节点 node 的后继节点 node.next
  • 待删除节点引用指向置空

节点的删除
直接调用该方法删除头结点或尾结点

// 删除头结点
public int removeHead() {
	return remove(0);
}
// 删除尾结点 
public int removeTail() {
	return remove(size - 1);
}

2.单链表典型题目

1). 删除链表中所有值为 val 的元素
思路:使用虚拟头结点以及双指针(一个在前一个在后),遍历链表,遇到与 val 相同的节点就删除。

public Node removeAllValue(int val) {
	// 创建一个虚拟头结点
	Node dummyHead = new Node();
	dummyHead.next = head; // 这个节点连接 head
	// 使用双指针
	Node node = dummyHead;
	Node cur = head;
	if (head == null) {
		return;
	}
	while (cur != null) {
		if (cur.val == val) {
			node.next = cur.next;
			break;
		}
		node = cur;
		cur = cur.next;
	}
	return dummyHead.next;
}

2). 反转链表
将给定的链表从尾到头重新反转排列,并返回反转后的链表

public Node reverseList(Node head) {
	Node dummy = new Node();
	dummy = null;
	Node cur = head;
	while (cur != null) {
		Node pre = cur.next;
		cur.next = dummy;
		dummy = cur;
		cur = pre;
	}
	return dummy;
}

从头节点开始到第一次完整循环结束示意图
链表反转
3).环形链表
给定一个链表,判断链表中是否有环
思路:使用快慢指针,若链表中存在环,则快指针一定会与慢指针相遇

public boolean hasCycle(Node head) {
	Node fast = head, slow = head;
	while (fast != null && fast.next != null) {
		// 快指针一次走两步
		fast = fast.next.next;
		// 慢指针一次走一步
		slow = slow.next;
		// 若相等,则证明快指针追上了慢指针,是环形链表
		if (fast == slow) {
			return true;
		}
	}
	// 若走出循环,则一定不是环形链表
	return false;
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值