链表特点:
1.每个元素都指向下一个元素
2.存储不连续
3.每个结点包含data区和next区
4.链表分带头节点以及不带头结点的,具体是否要带看需求(ps:我用的都带了)
链表类型:
1.单向链表
2.双向链表
3.循环链表
每个元素由一个节点构成,单向链表中,节点中保存的是数据和指向下一个的指针,双向链表中
,结点存储的是指向前一个指针,数据,指向后一个指针
下面贴上一个图~:
上图的循环链表看着像不像单向链表的改进? 像~~,就是尾指针不指向空了,指向自己的头~
不过实际上并不想你图里看到的,他们还有一个结点叫做哨兵节点,就是head头节点,因为他不存储数据,就是当个头头在那站着。但他指向第一个数据。
有头结点才能找到下一个元素,不然你从哪开始遍历链表?head可以简化边界判断(你咋知道这个是头?嗯,因为他有head特点嘿嘿)。
下面看性能:
随机访问:
根据index查找,时间复杂度为O(n).
插入或者删除:(这个得看位置了)
1.起始位置:O(1)
2.结束位置:已知尾节点的时候(双向链表已知尾结点)O(1),不知尾结点是哪个,就要从头找到尾:查找的时间复杂度O(n)+插入的时间复杂度O(1)。
3.其他位置:这个位置就得查找了,你插入或者删除指定位置数据得先根据index查找吧,这时间复杂度就高了:查找的时间复杂度O(n)+插入的时间复杂度O(1)。
这里先讲一下怎么进行插入和删除的吧:后面会进行代码实现。
首先我们要根据索引把位置找到:起始位置就在头后面插入,结束位置就在尾巴插入,这俩都不需要遍历。
根据索引查找到想要插入的数据时:假如在上图index-2后插入。index-2对应的数据是3,就让index-2所在节点的next指针指向要假如节点的数据,把要加如节点的next指针指向index-3节点数据。你不要晕~~~
删除数据时:index-2的next指针直接指向index-4
你看你看,咱们在数组中删除和插入数据的时候,后面的数据都得移动,毕竟数据得连续存储,这个链表就不需要,性能不就上升了嘛
单向链表类的设计:
public class List1 {
// 有头结点才能找到下一个元素,不然你从哪开始遍历链表?链表类的一个属性
private Node head;
//单向链表节点,有值和指向下一个节点指针
private class Node{
int value;
Node next;
// 为了更方便赋值给个有参数构造方法吧
public Node(int value,Node next){
this.value=value;
this.next=next;
}
}
// looklook!上面为啥把Node节点写里面?
// ---emm,写外面当然可以,这里设计到了java23中设计模式里面的知识:链表类和节点是组合关系,啥是组合关系,就是说,有头就得有嘴,头和嘴难舍难分。
// 这种强烈的关系就是组合关系。这种关系推荐写成内部类呦。这样更少的类对外暴露(类改成了private的了)。当然了写成外部类也行。只是从原则上讲,内部类更加符合原则而已。 也更加符合java的封装思想。
}
单向链表头部添加节点:
// 添加节点
public void addFirst(int value) {
// 都进来了,就得先符合结点特征啊,不然咋加入链表?包装一下:成为结点
// 链表可能为空
// head = new Node(value, null);#这里为空的时候,head就是null啊
// 链表非空
// head = new Node(value, head);
//单向链表添加节点就需要一行代码:
head = new Node(value, head);
}
遍历链表:
遍历链表有好几种方式:
1.while循环遍历
2.for循环遍历
3.实现Iterable循环
// 循环遍历while
public void loop(){
// 头结点明显的作用来了
Node p=head;
while(p!=null){
// 这里可以使用消费者接口,来简化输出。
System.out.println(p.value);
// 往下走
p=p.next;
}
}
// 循环遍历for
public void loop2(Consumer<Integer> consumer){
for(Node p=head;p!=null;p=p.next){
consumer.accept(p.value);
}
}
//循环遍历I实现接口terable
public class List1 implements Iterable<Integer> {
// 有头结点才能找到下一个元素,不然你从哪开始遍历链表?链表类的一个属性
private Node head;
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
Node p = head;
@Override
public boolean hasNext() {//是否有下一个元素
return p != null;
}
@Override
public Integer next() {//返回当前值,并指向下一个元素。
int value = p.value;
p = p.next;
return value;
}
};
}
private class Node {
int value;
Node next;
public Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
public static void main(String[] args) {
List1 integers = new List1();
for (Integer integer : integers) {
System.out.println(integer);
}
}
}
上面这个就是大名鼎鼎快捷键iter
的根源。有了Iterable才能使用iter
快捷键。
单向链表尾部添加:
// 尾部添加
public void end(int value){
Node node=new Node(value,null);
// 首先创建一个方法去寻找尾结点
Node last = findLast();
// 这里如果是空链表的话,那就是空链表,下面会报错,得避免这种情况
if(last==null){
head=node;
return;
}
last.next=node;
}
private Node findLast(){
Node p=head;
Node last=p;
while(p!=null){
if(p.next==null){
last=p;
}
p=p.next;
}
return last;
}
单向链表根据索引获取结点值:
链表其实本来是没有索引的,这里锁指的索引其实就是在遍历的过程中逐渐的增加的。
// 查找节点
public Node getNode(int index) {
int i = 0;
for (Node p = head; p != null; p = p.next) {
if (i == index) {
return p;
}
i++;
}
// 如果是空链表
return null;
}
单向链表根据索引获取结点value值:
// 根据索引查找节点数据
public Integer getByIndex(int index) {
int i = 0;
Node node = getNode(index);
if(node==null){
System.out.println("未查找到该索引所在的结点");
throw new IllegalArgumentException("索引数值不合法");
}
return node.value;
}
单向链表插入(向指定索引插入数据):
// 向指定索引插入数据
public void insertByIndex(int index,int value){
// index=0
if(index==0){
addFirst(value);//调用的方法,头部插入
return;
}
// 在某一个节点插入数据,得找到该节点的前一个节点(发现这里index=0的时候肯定会报错,就在上面避免这个情况)
Node pre = getNode(index-1);
if(pre!=null){
// 此时没找到该索引,就在末尾插入
throw new IllegalArgumentException("该索引错误,找不到该位置的结点");
}
// 在该节点后面插入
Node node1=new Node(value,pre.next);
pre.next=node1;
}
单向链表按照顺序插入:
package a_heima.beginKnow;
class HspList {
HeroNode head=new HeroNode(0,"",0,null);
// 根据编号进行插入
public static void main(String[] args) {
HeroNode h1=new HeroNode(1,"zyh1",18,null);
HeroNode h2=new HeroNode(2,"zyh2",19,null);
HeroNode h3=new HeroNode(3,"zyh3",11,null);
HeroNode h4=new HeroNode(4,"zyh4",12,null);
HeroNode h5=new HeroNode(5,"zyh5",13,null);
//创建一个链表
HspList hap=new HspList();
// hap.loop();
hap.insertByNo(h1);
hap.insertByNo(h5);
hap.insertByNo(h3);
hap.loop();
}
// 遍历
public void loop(){
HeroNode node=head;
if(head.next==null){
System.out.println("空链表=================");
return;
}
while(true){
node=node.next;
System.out.println(node);
if(node.next==null){
break;
}
}
}
// 按照顺序插入
public void insertByNo(HeroNode hero){
// 先判断链表是不是空,再找到要插入的位置,然后插入
HeroNode heroNode=head;
Boolean flag=true;
while(flag){
if(heroNode.next==null){
heroNode.next=hero;
break;
} else if (heroNode.next.no>hero.no) {
hero.next=heroNode.next;
heroNode.next=hero;
break;
} else if (heroNode.no==hero.no) {
flag=false;
}
heroNode=heroNode.next;
}
}
}
class HeroNode{
int no;
String name;
int age;
HeroNode next;
public HeroNode(int no,String name,int age,HeroNode next){
this.no=no;
this.name=name;
this.age=age;
this.next=next;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", age=" + age +
", next=" + next +
'}';
}
}
还有一个删除没写,思路都是差不多的,找到指定数据的位置,然后把前一个节点的next指针指向该数据的next节点。我这里就不演示了嘿嘿。
特点:
1.双向链表可以向前或向后查询
2.单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以前面我们单链表删除节点时,总是找到要删除的前一个节点的下一个节点来删除的。
和单向链表操作都差不多,就是多了个pre指针
在删除的时候会多几步。加入删除this节点,就是需要this.pre.next=this.next;
this.next.pre=this.pre;
上面两句代码用图具体解释一下:
在上面图的基础上载加上:
this.pre.next=this.next;
this.next.pre=this.pre;
这两句代码就可以明确什么是删除了。
代码演示:
package a.beginKnow;
/**
* @ClassName HspDoubleList
* @Description TODO 双向链表
* @Author zyhh
* @version: 1.0
*/
public class HspDoubleList {
DoubleNode head=new DoubleNode(0,"",0,null,null);
public static void main(String[] args) {
DoubleNode h1=new DoubleNode(1,"zyh1",18,null,null);
DoubleNode h2=new DoubleNode(2,"zyh2",19,null,null);
DoubleNode h3=new DoubleNode(3,"zyh3",11,null,null);
DoubleNode h4=new DoubleNode(4,"zyh4",12,null,null);
DoubleNode h5=new DoubleNode(5,"zyh5",13,null,null);
DoubleNode h6=new DoubleNode(8,"zyh8",16,null,null);
HspDoubleList hspDoubleList = new HspDoubleList();
hspDoubleList.loop();
hspDoubleList.add(h1);
hspDoubleList.add(h2);
hspDoubleList.add(h3);
// hspDoubleList.add(h4);
// hspDoubleList.add(h5);
// hspDoubleList.add(h6);
hspDoubleList.loop();
System.out.println("===========================");
DoubleNode h7=new DoubleNode(1,"zbf",18,null,null);
hspDoubleList.update(h7);
hspDoubleList.loop();
System.out.println("============================");
hspDoubleList.delete(h3);
hspDoubleList.loop();
}
// 添加
public void add(DoubleNode doubleNode){
System.out.println("add=============");
// 遍历到最后,在最后添加
// if(head.next==null){
// System.out.println("空链表============");
// return;
// }
DoubleNode temp = head;
while(true){
if(temp.next==null){
temp.next=doubleNode;
doubleNode.pre=temp;
return;
}
temp=temp.next;
}
}
// 删除节点
public void delete(DoubleNode doubleNode){
// 好事要判断是否为空,然后遍历找到
if(head.next==null){
System.out.println("空链表==========");
return;
}
Boolean flag=false;
DoubleNode temp=head;
while(true){
if(temp.no== doubleNode.no){
flag=true;
// 假如要删除的是最后一个节点
if(temp.next==null){
temp.pre.next=null;
}else{
temp.next.pre=temp.pre;//这里我把上一个节点给了下一个节点的上一个节点
temp.pre.next=temp.next;//这里我把上一个节点的下一个节点的给了该节点的下一个节点
}
return;
}
// 如果都找到最后一位都还没找到,那别找了,出来吧,肯定是穿进来的数据都不在链表中
if(!flag&&temp.next==null){
System.out.println("未找到指定节点,请检查数据");
return;
}
temp=temp.next;
}
}
// 修改
public void update(DoubleNode doubleNode){
if(head.next==null){
System.out.println("空链表=====");
return;
}
DoubleNode temp=head;
Boolean flag=true;
while(true){
if(temp.no==doubleNode.no){
break;
} else if (temp.next==null) {//如果找到最后都没有
flag=false;
break;
}
temp=temp.next;
}
if(flag){
temp.age=doubleNode.age;
temp.name= doubleNode.name;
} else{
System.out.println("未找到匹配的号,请检查是否正确!");
}
}
// 遍历(和单链表一样)
public void loop(){
DoubleNode node=head;
if(head.next==null){
System.out.println("空链表=================");
return;
}
while(true){
node=node.next;
System.out.println(node);
if(node.next==null){
break;
}
}
}
}
class DoubleNode{
int no;
String name;
int age;
DoubleNode next;
DoubleNode pre;
public DoubleNode(int no,String name,int age,DoubleNode next,DoubleNode pre){
this.no=no;
this.name=name;
this.age=age;
this.next=next;
}
@Override
public String toString() {
return "DoubleNode{" +
"no=" + no +
", name='" + name + '\'' +
", age=" + age +
// ", next=" + next +
", pre=" + pre +
'}';
//注意!!!!!!!!!!!!!!!!!!!!!!
// TODO:这个toString()方法里面的next和pre节点二选一打印输出,不然就会爆出栈溢出异常。
// TODO:你想呀,如果你一致next,没指向next节点在到达末尾之前会有另一个节点,达到末尾了就不输出了。pre同理,总会走到嘴开头
// TODO:但是!!!要是你俩都写的话,加入你在头节点next,那么next节点还是会有next和pre节点,假如你的next节点走到最后了,但你
// 还要打印pre节点,就又拐回来了,形成了一个递归调用,造成栈溢出异常。
}
}
上面的双向链表增删该查中有一个需要注意的点搁这里拿出来说一下:
:我在双向链表中为了方便节点打印就重写了toString()方法(挖坑了),习惯性的快捷键~然后我就在遍历的时候使用的toString()方法打印了节点信息。就完美的栽坑里了。
如果你在重写 toString() 方法时遇到了栈溢出异常,可能是因为你在 toString() 方法中无限循环地访问了节点的相邻节点,造成了无限递归调用 toString() 方法。
当你在 toString() 方法中调用了 System.out.println(temp); 来打印节点信息时,temp.toString() 方法会被自动调用。如果在 toString() 方法中又调用了相邻节点的 toString() 方法,就会形成无限的递归调用,导致栈溢出异常。
为了解决这个问题,你可以改进 toString() 方法的实现,避免无限循环调用。确保你只打印节点的信息,而不再递归地调用相关方法。以下是一个示例:
@Override
public String toString() {
return "DoubleNode{" +
"no=" + no +
", name='" + name + '\'' +
", age=" + age +
// ", next=" + next +
", pre=" + pre +
'}';
//注意!!!!!!!!!!!!!!!!!!!!!!
// TODO:这个toString()方法里面的next和pre节点二选一打印输出,不然就会爆出栈溢出异常。
// TODO:你想呀,如果你一致next,没指向next节点在到达末尾之前会有另一个节点,达到末尾了就不输出了。pre同理,总会走到嘴开头
// TODO:但是!!!要是你俩都写的话,加入你在头节点next,那么next节点还是会有next和pre节点,假如你的next节点走到最后了,但你
// 还要打印pre节点,就又拐回来了,形成了一个递归调用,造成栈溢出异常。
}
这里就不得不提出一个很著名的问题叫做约瑟夫环:
n个人坐到一个圈,从约定的编号k开始数,数到m的人嘎掉,下一个再从1开始数,再数到m再嘎掉,直到剩下一个人。
下面我用
n=5(共5人围成圈)
k=1(从第一个人开始报数)
m=2(数到2的人嘎掉)
思路:
创建:
先创建第一个节点,让first指向该节点,并形成环。
后面当我们没创建一个新的节点,就把该节点加入到已有的环中即可。
遍历:
先让一个辅助指针current指向first节点。
然后通过一个while循环遍历该换形链表即可,如果current==first遍历结束。
关于约瑟夫环人出圈就是一个比较难的问题了,因为我们不仅要让这个人出圈,还要让这个人的下一个人从1开始数数。
解决该问题的大概思路如下:
需要创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点
报数前,先把helper和first移动k-1
当小孩报数时,让first和helper指针同时移动m-1次【注意这个m上面我们已经提到了,是小孩报数时要出圈的数。】
这时就可以将first指向的小孩节点出圈。
即:first=first.next;helper.next=first;
此时哪个被出圈的节点就没了。
好了,思路说完了,是不是有点迷糊。哈哈哈!!!说一下可能不理解的部分:
问:
报数前,先把helper和first移动k-1是什么鬼?
答:
k表示从第几个人开始报数。加入我们要从第二个人开始报数,那么得让first指针指向开始报数的那个人,方便报数开始移动。helper是一个辅助指针在first的隔壁,所以俩都得移动k-1啊。
问:
helper和first指针同时移动m-1次是什么鬼?
答:
首先我们弄清楚,helper指向的是最后一个节点,而first在第一个节点。所以helper指向的和first指向的节点就是在隔壁啊!!!对吧,然后这俩都往后移动m-1,这个时候first指向的就是要被嘎的那个,helper指向的是要被嘎的前一个啊!然后有经过first=first.next;helper.next=first;
这么一操作,不就刚好在链表中被删除了嘛~
代码实现:
package a_heima.beginKnowLianBiao;
/**
* @ClassName Josephu
* @Description TODO 约瑟夫环(单向环形链表)
* n=5(共5人围成圈)
* k=1(从第一个人开始报数)
* m=2(数到2的人嘎掉)
* @Author zyhh
* @version: 1.0
*/
//需要创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点
//当小孩报数时,让first和helper指针同时移动m-1次
// 这时就可以将first指向的小孩节点出圈。
// 即:first=first.next;helper.next=first;
//此时哪个被出圈的节点就没了。
public class Josephu {
public static void main(String[] args) {
CircleSingleList circleSingleList = new CircleSingleList();
circleSingleList.addBoy(5);
circleSingleList.showBoy();
System.out.println("======出圈====");
circleSingleList.outQueue(1,2,5);
}
}
class CircleSingleList{
// 创建一个first节点,当前没有编号。
private Boy first=new Boy(-1);
// 出圈
/**
*
* @param startNo 表示从第几个小孩开始数数
* @param countNum 表示数几下
* @param nums 表示最初有多少小孩在圈中
*/
public void outQueue(int startNo,int countNum,int nums){
// 数据校验
if(first==null||startNo<1||startNo>nums){
System.out.println("信息有误,无法进行出圈淘汰操作!");
return;
}
// 创建一个辅助指针,帮助节点出圈操作
Boy helper=first;
// 把辅助指针移动到最后一个节点
while (true){
if(helper.getNext()==first){
break;
}
helper=helper.getNext();
}//此时while循环结束,helper就是在最后一个位置了。
for (int i = 0; i < startNo-1; i++) {// 报数前,先移动k-1次
first=first.getNext();
helper=helper.getNext();
}
// 当小孩报数时,让first和helper移动m-1次,然后出圈。
// 这里是一个循环操作,直到圈中只有一个节点
while (true){
if(helper==first){
break;//说明此时圈中只有一个节点
}
// 此时让,first和helper指针同时移动m-1次
for (int i = 0; i < countNum-1; i++) {
first=first.getNext();
helper=helper.getNext();
}//此时first指向的节点就是要出圈的节点。
System.out.println("查看出圈的节点编号:"+first.getNo());
//下面两行代码是出圈操作
first=first.getNext();
helper.setNext(first);
}
System.out.println("打印圈中在最后留下来的节点的编号:"+first.getNo());
}
// 添加小孩节点,构建一个环形链表
public void addBoy(int value){
if(value<1){//数据校验
System.out.println("nums的值不正确");
return;
}
Boy current=null;//辅助指针,帮助构建环形链表
for (int i=1;i<=value;i++){
// 根据编号,创建小孩节点
Boy boy=new Boy(i);
// 如果是第一个小孩
if(i==1){
first=boy;//这里表示把first指向了boy节点
first.setNext(first);//构成环状
current=first;//让当前指针指向第一个小孩
}else {
current.setNext(boy);
boy.setNext(first);
current=boy;
}
}
}
// 遍历当前环形链表
public void showBoy(){
if(first.getNext()==null){
System.out.println("链表为空");
return;
}
// 因为first不能动,因此我们仍然需要一个辅助指针完成遍历
Boy current=first;
while (true){
System.out.println("遍历的节点编号为:"+current.getNo());
current=current.getNext();
if(current.getNo()==first.getNo()){
System.out.println("遍历结束");
break;
}
}
}
}
class Boy{
private int no;//编号
private Boy next;//指向下一个指针,默认为null
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
下面贴上运行截图: