前言
-🏀大家好,我是BXuan,热爱编程与篮球的软件工程大二学生一名
-📚近期在准备4月份的蓝桥省赛,本章与大家一起聊聊有关链表的问题!如文有误,请大家指出并多多包涵。
-🏃放弃不难,但坚持一定很酷。
文章目录
🚩概念
①链表是线性表的链式存取的数据结构,是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
②链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:数据域(数据元素的映象)+ 指针域(指示后继元素存储位置),数据域就是存储数据的存储单元,指针域就是连接每个结点的地址数据。
🚩顺序存储VS链式存储
-
顺序存储
-
链式存储
🚩顺序表VS链表
顺序表
优点:
-
无需为表示结点间的逻辑关系而增加额外的存储空间(因为逻辑上相邻的元素其存储的物理位置也是相邻的);
-
可方便地随机存取表中的任一元素:由于顺序表每个元素的大小相等,且知道第几个元素就可以通过计算得到任意元素的地址,既可以随机存取任一元素。
缺点:
-
插入或删除运算不方便:除表尾的位置外,在表的其它位置上进行插入或删除操作都必须移动大量的结点,其效率较低;
如在 7、8 之 间插入 X 元素,那我们为了保证其顺序性需要把 8 和 9 向后移动一位,再将 X 放到 8 的位置。
这样的存储方式增加了处理器和 IO 资源的消耗代价,这是我们不愿意看到的,至于删除其原理相同。
-
难以匹配存储规模:
由于顺序表要求占用连续的存储空间,存储分配只能预先进行静态分配,因此当表长变化较大时,难以确定合适的存储规模。
时间复杂度
查找操作为 O(1),插入和删除操作为 O(n)。
链表
优点:
- 插入和删除速度快,保留原有的物理顺序,在插入或者删除一个元素的时候,只需要改变指针指向即可;
- 没有空间限制, 存储元素无上限, 只与内存空间大小有关;
- 动态分配内存空间,不用事先开辟内存;
- 使内存的利用率变高。
缺点:
- 占用额外的空间以存储指针,比较浪费空间,不连续存储,Malloc 函数开辟空间碎片比较多;
- 查找速度比较慢,因为在查找时,需要循环遍历链表。
时间复杂度:
查找操作为 O(n), 插入和删除操作为 O(1)。
🚩知识点
- 单链表实现原理与应用
- 循环链表实现原理与应用
- 双向链表实现原理与应用
一、小王子(单链表)
问题描述
小王子有一天迷上了排队的游戏,桌子上有标号为 1−10 的 10 个玩具,现在小王子将他们排成一列,可小王子还是太小了,他不确定他到底想把那个玩具摆在哪里,直到最后才能排成一条直线,求玩具的编号。已知他排了 M 次,每次都是选取标号为 X 个放到最前面,求每次排完后玩具的编号序列。
要求一:采用单链表解决
输入描述
第一行是一个整数 M,表示小王子排玩具的次数。
随后 M 行每行包含一个整数 X,表示小王子要把编号为 X 的玩具放在最前面。
输出描述
共 M 行,第 i 行输出小王子第 i 次排完序后玩具的编号序列。
运行限制
- 最大运行时间:1s
- 最大运行内存: 128M
输入输出案例
输入
5
3
2
3
4
2
输出
3 1 2 4 5 6 7 8 9 10
2 3 1 4 5 6 7 8 9 10
3 2 1 4 5 6 7 8 9 10
4 3 2 1 5 6 7 8 9 10
2 4 3 1 5 6 7 8 9 10
代码示例
import java.util.Scanner;
//1:无需package
//2: 类名必须Main, 不可修改
public class Main {
// 构建单链表结构
static class Node{
int data; // 数据域
Node next; // 指针域
// 传入当前链表的数据域的值
Node(int v){
data = v;
}
}
// 将头节点单独插入到链表中
static Node head = new Node(1);
// 构建插入函数
static void init(){
// 首先将
Node x = head;
for(int i = 1;i <= 10;i++){
x=(x.next = new Node(i));
}
x.next = null;
}
//然后选中的数字 就在链表中将其删除,然后重新插入到头节点后面
static void del(int x){
Node before = head;
for(Node T = head.next;T != null;T = T.next){
if(T.data == x){
//在删除之前需要将临时结点保存然后方便后续进行插入
Node temp = T;
before.next = T.next;
//结束函数
return;
}else{
before = T;
}
}
}
// 插入函数
static void insert(int x){
Node temp = new Node(x);
temp.next = head.next;
head.next = temp;
}
// 打印函数
static void show(int i){
for(Node T = head.next;T != null;T = T.next){
System.out.print(T.data + " ");
}
System.out.println(" ");
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//在此输入您的代码...
// 输入整数M
int M = scan.nextInt();
// 构建单链表
init();
// 构建一个数组进行存储
int[] arr = new int[M];
for(int i = 0;i < arr.length;i++){
arr[i] = scan.nextInt();
}
// 打印
for(int i = 0;i < M;i++){
int X = arr[i];
del(X);
insert(X);
show(i);
}
scan.close();
}
}
二、约瑟夫环的问题
问题描述
设有 n 个人围坐在圆桌周围,现从某个位置 k 上的人开始报数,报数到 m 的人就站出来。下一个人,即原来的第 m+1 个位置上的人,又从 1 开始报数,再报数到 m 的人站出来。依次重复下去,直到全部的人都站出来为止。试设计一个程序求出这 n 个人的出列顺序。
要求一:采用循环链表解决。
要求二:可以使用模拟法,模拟循环链表。
要求三:可以不使用循环链表类的定义使用方式。
输入描述
输入只有一行且为用空格隔开的三个正整数 n,k,m,其含义如上所述。
输出描述
共 n 行,表示这 n 个人的出列顺序。
输入输出案例
输入
3 5 8
输出
3
2
1
运行限制
- 最大运行时间:1s
- 最大运行内存: 128M
代码示例
import java.util.Scanner;
public class Main{
static class Node
{
int val;
Node next;
Node(int v)
{
val = v;
}
} //成员类,代表节点,类似于C++语言中的结构体
public static void main(String[] args)
{
int N, M, K; //n个人从k位置开始报数,数到m出列
Scanner in = new Scanner(System.in);
N = in.nextInt();
K = in.nextInt();
M = in.nextInt();
if (M == 1)
{
for (int i = 0; i < N; i++) //共计N个人
{
int left;
left = K - 1;
if (left == 0)
left = N;
System.out.print(left);;
}
}
/*
如果叫到1就出列,那么就是谁喊谁出列,以上为处理方式
*/
else
{
Node t = new Node(1); //头节点单列出来,方便形成循环链表
Node x = t;
/*
以上为信息输入,和所续变量的声明
*/
for (int i = 2; i <= N; i++)
x = (x.next = new Node(i)); //建立单向链表
x.next = t; //最后一个节点的next指向第一个节点,形成循环链表
for (int i = 1; i <= K - 1; i++) //寻找报数的起点
x = x.next;
while (x != x.next)
{ //只剩下一个结点的时候停止
for (int i = 1; i < M; i++)
x = x.next;
//此时x是将出列的节点的前一个节点
System.out.println(x.next.val);
x.next = x.next.next;
}
System.out.println(x.val);
}
}
}
三、小王子(双链表)
问题描述
小王子有一天迷上了排队的游戏,桌子上有标号为 1−10 的 10 个玩具,现在小王子将他们排成一列,可小王子还是太小了,他不确定他到底想把那个玩具摆在哪里,直到最后才能排成一条直线,求玩具的编号。已知他排了 M 次,每次都是选取标号为 X 个放到最前面,求每次排完后玩具的编号序列。
要求一:采用循环链表解决
输入描述
第一行是一个整数 M,表示小王子排玩具的次数。
随后 M 行每行包含一个整数 X,表示小王子要把编号为 X 的玩具放在最前面。
输出描述
共 M 行,第 i 行输出小王子第 i 次排完序后玩具的编号序列。
输入输出案例
输入
5
3
2
3
4
2
输出
3 1 2 4 5 6 7 8 9 10
2 3 1 4 5 6 7 8 9 10
3 2 1 4 5 6 7 8 9 10
4 3 2 1 5 6 7 8 9 10
2 4 3 1 5 6 7 8 9 10
运行限制
- 最大运行时间:1s
- 最大运行内存: 128M
代码示例
package E_lanqiao;
import java.util.Scanner;
//1:无需package
//2: 类名必须Main, 不可修改
public class Main {
//类似于C链表结构体
static class Node{
int data;
Node nextNode;
Node beforeNode;
Node(int v){
data = v;
}
}
// 头节点单独进行插入
static Node head = new Node(1);
// 对后续双链表进行构造
private static void init() {
// TODO Auto-generated method stub
head.beforeNode = null;
Node xNode = head;
Node tNode = head;
for(int i = 1;i <= 10;i++) {
xNode = (xNode.nextNode = new Node(i));
xNode.beforeNode = tNode;
tNode = xNode;
}
xNode.nextNode = null;
}
// 删除结点
private static void del(int x) {
Node beginNode = head;
Node beginNode2 = head;
Node afterNode = beginNode2.nextNode;
for(Node tNode = head.nextNode;tNode != null;tNode = tNode.nextNode) {
if(beginNode.data == x) {
beginNode2.nextNode = beginNode.nextNode;
afterNode.beforeNode = beginNode2;
return;
}else {
beginNode2 = beginNode;
beginNode = beginNode.nextNode;
afterNode = beginNode.nextNode;
}
}
}
// 重新插入结点
private static void insert(int x) {
Node tempNode = new Node(x);
Node seconNode = head.nextNode;
tempNode.nextNode = head.nextNode;
head.nextNode = tempNode;
tempNode.beforeNode = head;
seconNode.beforeNode = tempNode;
}
// 打印函数
private static void show() {
for (Node tNode = head.nextNode; tNode != null; tNode=tNode.nextNode) {
System.out.print(tNode.data+" ");
}
System.out.println();
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//在此输入您的代码...
// 输入小王子排玩具的次数
int M = scan.nextInt();
// 构建链表
init();
// 创建数组保存需要调换的数字
int[] arr = new int[M];
for (int i = 0; i < M; i++) {
arr[i] = scan.nextInt();
}
// 对数组循环一遍调换数字
for (int i = 0; i < arr.length; i++) {
del(arr[i]);
insert(arr[i]);
show();
}
scan.close();
}
}
👏小结
本次学习了三种最常见的链表的原理与使用方式,可见链表的种类是千变万化的,像是循环链表与双向链表的结合形成的双向循环链表,存储图的十字链表等,我们学好这基础的三种链表,以不变应万变才是正确的面对方式。冲冲冲!