一、需掌握知识点
.单链表实现原理与应用
.双向链表实现原理与应用
.循环链表实现原理与应用
二、为什么使用链表
相信大家在这之前已经学过数组,无论是 C++,Java,Python 还是其它语言大都会有数组这一概念,好用吗?很好用,所谓数组其实就是线性表的顺序存储形式的原理,我们来看一下链表的定义并对比一下链式存储与顺序存储的存储方式。
1.什么是链表
链表是线性表的链式存取的数据结构,是一种链式存取的数据结构,是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:数据域(数据元素的映象)+ 指针域(指示后继元素存储位置),数据域就是存储数据的存储单元,指针域就是连接每个结点的地址数据。 相比于线性表顺序结构,操作复杂。
似乎定义是有些晦涩难懂,我们用两张图来对比一下数组也就是线性表的顺序存储结构和链表在内存中存储 1-9 号元素的形式:
顺序存储
链式存储
思考一下:
线性表的数据存储方式的内存地址是顺序的,链式存储的数据的内存地址的有什么规律呢?
事实上链式存储的内存地址是随机分配的,他们每个节点地址之间是没有任何关联的。而且在每个新的节点在产生之前,我们都是不知道他的地址的。
链表初体验
小王子有一天迷上了排队的游戏,桌子上有标号为 1-10 按顺序摆放的 10 个玩具,现在小王子想将它们按自己的喜好进行摆放。小王子每次从中挑选一个好看的玩具放到所有玩具的最前面。已知他总共挑选了 M 次,每次选取标号为 X 的玩具放到最前面,求摆放完成后的玩具标号。 给出一组输入,M=8 共计排了 8 次,这 8 次的序列为 9,3,2,5,6,8,9,8。 求最终玩具的编号序列。 | |
---|---|
解题思路
首先:我们需要初始序列,生成一个初始链表
static class Node {
int data;
Node next;
Node(int v) {
data = v;
}
}//成员类,代表节点,类似于C++语言中的结构体
static Node head = new Node(0);//头节点单列出来
//形成单链表
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 Befor = head; //用于存放当前节点的前驱,因为单链表单向遍历,我们不能从下一个找到上一个
for (Node T = head.next; T != null; T = T.next) //链表的遍历常用写法
{
if (T.data == x) //找到要的那个数了
{
Node temp = T; //先临时保存结点
Befor.next = T.next; //将节点从链表上摘除
return; //删除结束后,结束函数。
}
Befor = 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(" ");
}
总体代码:
package Fourteen;
import java.util.Scanner;
public class one_1 {
static class Node {
int data;
Node next;
Node(int v) {
data = v;
}
}//成员类,代表节点,类似于C++语言中的结构体
static Node head = new Node(0);//头节点单列出来
//形成单链表
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 Befor = head; //用于存放当前节点的前驱,因为单链表单向遍历,我们不能从下一个找到上一个
for (Node T = head.next; T != null; T = T.next) //链表的遍历常用写法
{
if (T.data == x) //找到要的那个数了
{
Node temp = T; //先临时保存结点
Befor.next = T.next; //将节点从链表上摘除
return; //删除结束后,结束函数。
}
Befor = 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) {
int N;
Scanner in = new Scanner(System.in);
init(); //构建初始链表
N = in.nextInt();
for (int i = 0; i < N; i++) {
int x = in.nextInt();
del(x);
insert(x);
show(i);
}
}
}
结果:
常见的时间复杂度量级如下:
2.链表
优点:
插入和删除速度快,保留原有的物理顺序,在插入或者删除一个元素的时候,只需要改变指针指向即可;
没有空间限制, 存储元素无上限, 只与内存空间大小有关;
动态分配内存空间,不用事先开辟内存;
使内存的利用率变高。
缺点:
占用额外的空间以存储指针,比较浪费空间,不连续存储,Malloc 函数开辟空间碎片比较多;
查找速度比较慢,因为在查找时,需要循环遍历链表。
时间复杂度:
查找操作为 O(n), 插入和删除操作为 O(1)。
3.循环链表
定义
将单链表或者双链表的头尾结点链接起来,就是一个循环链表。不增加额外存储花销,却给不少操作带来了方便从循环表中任一结点出发,都能访问到表中其他结点。
使用循环链表解决约瑟夫环问题
设有 n 个人围坐在圆桌周围,现从某个位置 k(1≤k≤n) 上的人开始报数,报数到 m 的人就站出来。下一个人,即原来的第 m+1 个位置上的人,又从 1 开始报数,再报数到 m 的人站出来。依次重复下去,直到全部的人都站出来为止。试设计一个程序求出这 n 个人的出列顺序。
解题思路
首先:生成一个循环链表
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结点是报数为1的前一个结点
x = x.next;
最后:删除应该删除的元素,继续报号
while (x != x.next)
{ //只剩下一个结点的时候停止
for (int i = 1; i <M; i++) x = x.next;
//此时x是将出列的节点的前一个节点
System.out.print(x.next.val + " ");
x.next = x.next.next;
}
System.out.println(x.val);
代码
package Fourteen;
import java.util.Scanner;
public class one_2
{
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 input = new Scanner(System.in);
N = input.nextInt();
K = input.nextInt();
M = input.nextInt();
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.print(x.next.val + " ");
x.next = x.next.next;
}
System.out.println(x.val);
}
}
截图
4.双向链表
双链表和单链表的解决办法是一样的,只是在一些结点上可以很好的找到前驱元素和后继元素
代码:
package Fourteen;
import java.util.Scanner;
public class one_3
{
static class Node
{
int data;
Node next;
Node before;
Node(int v)
{
data = v;
}
} //成员类,代表节点,类似于C++语言中的结构体
static Node head = new Node(1); //头节点单列出来
static void init()
{
Node x = head;
for (int i = 1; i<= 10; i++)
{
x.next = new Node(i); //建立双向链表
x.next.before = x;
x = x.next;
}
x.next = null;
}
static void del(int x)
{
for (Node T = head.next; T != null; T = T.next) //链表的遍历常用写法
{
if (T.data == x) //找到要的那个数了
{
T.before.next = T.next; //将节点从链表上摘除
T.next.before=T.before;
return; //删除结束后,结束函数。
}
}
}
static void insert(int x)
{
Node temp = new Node(x);
temp.next = head.next;
temp.next.before = temp;
head.next = temp;
}
static void show(int i)
{
// System.out.println("这是第" + 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)
{
int N; //进行N次的移动
Scanner in = new Scanner(System.in);
init();
N = in.nextInt();
for (int i = 0; i < N; i++)
{
int x = in.nextInt();
del(x);
insert(x);
show(i);
}
}
}
截图