链表:
单链表的存储地址是连续的,但是每一个节点(data域和next域)保存下一个元素的地址不是连续的,因此单链表的各个节点不一定是有序存储而是链式存储。
链表的访问需要一个一个进行遍历访问查找next域的值是否与待访问的值相同,因此访问的时间复杂度是O(N);
链表的搜索需要一个一个进行遍历查找next域的值是否与待搜索的值相同,因此访问的时间复杂度是O(N);
链表的插入、删除为O(1),断开指针即可,如果需要在指定位置插入、删除元素需要的时间复杂度为O(N)
可以得出链表的特点是写的很快,读的很慢
链表的逻辑结构表示:看似是一个有序存储,a1通过指针代表下一个域指向a2,实际上内存上a1后面并不一定存储就是a2,只是通过指针把它连成了一个单链表,在存储中是链式存储而不是顺序存储
使用带head头的单向链表的实现
添加的2个方式:
-
1、一个一个添加到链表的尾部
-
2、按照编号添加节点到指定位置
注意情况:已经找到添加的位置、已经在最后一个位置、新的节点编号与游标的下一个节点的编号相同时都不满足继续向下寻找的条件;
-
当tamp=head.next时,tamp==null表示链表已经遍历结束,而不是在最后一个节点,可以认为在最后一个节点的后面空位置处;例如链表只有一个元素,遍历开始,此时tamp就在第一个元素的位置tamp!=null,更新tamp,而tamp.next=null赋给tamp,那么这个时候tamp=null
-
tamp.no=newheronode.no表示tamp指向的节点的编号等于传入的新的节点编号,就去修改
-
如果tamp=head,当tamp.next=null,此时tamp指向的时最后一个节点。例如链表为空时,当tamp.next=null,tamp=head就表示头部。
import java.security.PublicKey;
import java.util.Stack;
public class SingleLinkedListDemo {
public static void main(String[] args) {
SingleLinkedList singleLinkedList = new SingleLinkedList();
HeroNode heroNode1 = new HeroNode("林冲", "豹子头", 1);
HeroNode heroNode2 = new HeroNode("宋江", "及时雨", 2);
singleLinkedList.addByOrder(heroNode1);
singleLinkedList.addByOrder(heroNode2);
singleLinkedList.show();
//获取倒数第1个节点测试代码
// System.out.println(singleLinkedList.findLastIndexNode(singleLinkedList.getHead(), 1));
System.out.println("======================");
//逆序链表打印
SingleLinkedList.reversePrint(singleLinkedList.getHead());
System.out.println("=======================");
//反转链表
SingleLinkedList.reverseList(singleLinkedList.getHead());
singleLinkedList.show();
}
}
//定义一个链表类,提供管理节点的方法:添加删除修改查找
class SingleLinkedList{
//初始化一个头节点,头节点不要动,也不存放数据,编号为0
private HeroNode head=new HeroNode("","",0);
//获取头节点
public HeroNode getHead() {
return head;
}
//链表的逆序打印:利用栈的先进后出的结构,将链表的所有元素放入栈中,再将栈内的元素打印就行
public static void reversePrint(HeroNode head){
if (head.next==null){
return;//空链表不打印
}
Stack<HeroNode> stack = new Stack<>();
HeroNode cur=head.next;//游标指向链表的第一个元素
//遍历链表给stack放入元素
while (cur!=null){
stack.add(cur);
cur=cur.next;
}
//遍历stack 打印元素,打印出来的元素是原链表的逆序元素,并且原链表结构并未改变
while (stack.size()>0){
System.out.println(stack.pop());//pop方法:出栈
}
}
//链表的反转 头删与头插的思想
public static void reverseList(HeroNode head){
if (head.next==null||head.next.next==null){
return;//当前链表为空或者只有一个元素无需反转
}
HeroNode reverseNode=new HeroNode("","",0);//新链表头节点
HeroNode cur=head.next;
HeroNode next=null;//用于保存cur的下一个节点 否则链表会断开
//cur=null表示旧链表遍历结束 元素插入到新链表结束
while (cur!=null){
//先保存cur的下一个节点
next=cur.next;
//旧链表与头元素断开----头删
cur.next=reverseNode.next;
//头插 将断开的元素插入新链表的头部
reverseNode.next=cur;
//cur后移到下一个元素继续头删和头插
cur=next;
}
head.next=reverseNode.next;
}
//求单链表有效节点的个数(带头结点的链表,有效节点的个数不包含头节点)
public static int getLength(HeroNode head){
int length=0;
if (head.next==null){
return length;//空链表
}
HeroNode cur=head.next;
while (true){
if (cur==null){break;}
length++;
cur=cur.next;
}
return length;
}
//查找单链表中倒数第index个节点
//思路:单链表只可以正向遍历,首先获取链表有效长度个数,再循环移动(length-index)个位置即可找到。
public HeroNode findLastIndexNode(HeroNode head,int index){
if (head.next==null){
return null;//链表为空没有找到
}
int length = SingleLinkedList.getLength(head);
if (index<=0||index>length){//index的校验
return null;
}
HeroNode cur=head.next;
//for循环定位倒数的index位置
//例如:找倒数第1个元素,有效元素个数length为3,那么从第一个元素开始要移动3-1=2个位置
for (int i = 0; i < length - index; i++) {
cur=cur.next;
}
return cur;
}
//添加节点到单向链表第一种方式,不考虑编号顺序:将传入的节点放入链表的尾部
public void add(HeroNode newNode){
//因为head节点不能动,需要一个辅助游标遍历来寻找到最后一个节点
HeroNode tamp=head;
while (true){
if (tamp.next==null){
//说明链表为空或者遍历到最后一个位置
break;
}
//没有到最后一个位置,后移向下遍历
tamp=tamp.next;
}
//除了循环体,tamp为最后一个节点位置,让tamp的next属性指向新的节点,添加成功
tamp.next=newNode;
}
//添加节点到单向链表第二种方式,根据编号的大小将节点添加到指定位置,如果编号已存在表示添加失败
public void addByOrder(HeroNode newNode){
//因为head节点不能动,需要一个辅助游标遍历来寻找新的节点的上一个节点位置
HeroNode tamp=head;
boolean flag=false;//flag表示添加的编号是否存在 默认不存在
while (true){
if (tamp.next==null){
//此时tamp在链表最后一个位置,新的节点的上一个节点位置位于最后一个位置,要退出循环体添加元素;
break;
}
if (tamp.next.number>newNode.number){
//此时tamp代表找到新节点的上一个节点的位置 退出循环体在tamp的后面添加元素
//例如编号1,4,将2插入进去,需要找到1。因为1.next=4,4>2,所以找到了1,此时tamp=1
break;
}
if (tamp.next.number==newNode.number){
//tamp.next.number:tamp=head一开始tamp是头部,头部编号为0,因此遍历查找从next开始遍历
//比如链表内有1、2,想要添加1,那么tamp=0,tamp.next=1,编号已存在;
//说明编号已经存在
flag=true;
break;
}
//不满足以上条件 后移
tamp=tamp.next;
}
if (flag){
System.out.printf("编号%d已经存在,无法添加",newNode.number);
}else{
//先写新节点的尾部指向
newNode.next=tamp.next;
tamp.next=newNode;
}
}
//修改节点的信息 根据no编号来修改,即只能修改name和nickname
public void update(HeroNode newheronode){
boolean flag=false;//
if (head.next==null){
System.out.println("链表为空~");
return;
}
//链表不为空说明可以从第一个元素开始遍历 head.next表示第一个元素
HeroNode tamp=head.next;
while (true){
if (tamp.number==newheronode.number){
flag=true;
break;
}
if (tamp==null){
//tamp此时表示在链表最后一个元素的后一个空位置 表示链表遍历结束 tamp已经为空了,如果使用tamp.next就会报空指针异常
break;
}
tamp=tamp.next;
}
if (flag){
tamp.name=newheronode.name;
tamp.nickname=newheronode.nickname;
}else{
System.out.println("没有找到该元素");
}
}
//删除节点
//思路:head不能动,需要tamp来找到待删除节点newHeroNode的上一个节点位置,因此tamp一开始是head
//比较时是用 tamp.next.number与newHeroNode.number来进行比较
public void del(int number){
HeroNode tamp=head;
boolean flag=false;
while (true){
if (tamp.next==null){ //tamp在最后一个元素位置
break;
}
if (tamp.next.number==number){//找到了待删除节点newHeroNode的上一个节点位置tamp
flag=true;
break;
}
tamp=tamp.next;
}
if (flag){
tamp.next=tamp.next.next;
}else {
System.out.println("没有找到待删除节点");
}
}
//显示链表
public void show(){
if (head.next==null){
System.out.println("链表为空~");
}
HeroNode tamp=head.next;
while (true){
if (tamp==null){
//循环终止条件
//说明遍历链表结束 tamp就是最后一个元素的空位置处,不是最后一个元素
break;
}
System.out.println(tamp);
tamp=tamp.next;
}
}
}
//定义HeroNode类,每个对象是一个节点,一个节点包含数据域和下一个节点next域
class HeroNode{
public String name;
public String nickname;
public int number;
public HeroNode next;//下一个节点
public HeroNode(String name, String nickname, int number) {
this.name = name;
this.nickname = nickname;
this.number = number;
}
//为了显示方法 重写toString
@Override
public String toString() {
return "HeroNode{" +
"name='" + name + '\'' +
", nickname='" + nickname + '\'' +
", number=" + number +
'}';
}
}
leetCode203:移除链表元素
思路:使用临时节点dummy保存head节点,然后使用head节点来进行遍历,如果head已经遍历到待移除的元素,使用previous节点先保留在head移动之前的哪个节点,然后再移动head到下一个节点(head=head.next),这样在移除元素时就可以获取这个元素的上一个节点与下一个节点,让这个元素的上一个节点指向这个元素的下一个节点即可。
leetCode206:反转单链表
思路:头删和头插的思想,使用一个新的head头部,使用游标遍历旧链表,每遍历一个元素就取出放入新链表的最前端,最后让旧链表头部指向新链表头部。
注意点:游标指向第一个元素,需要使用一个Node节点先保存游标的下一个节点,当游标头插入新链表时,cur.next可以找到旧链表。
12345–>21345 (先反转2)–>32145(反转3): 比如一开始head指向1,dummy指向head,
dummy的指向为待移动的元素。比如此刻先移动2,就让dummy指向2;
待移动元素的上一个元素指向带移动元素的下一个元素;
待移动元素再指向链表的头部dummy.next
dummy先指向head.next(2处),(dummy.next=head.next);让head(1)指向3处(head.next=head.next.next);再让2指向1,(head.next.next=dummy.next)
public Node reverseHead(Node head){
if(head==null){
return null;
}
Node cur=head.next;
Node next=null;
Node reverseHead=new Node();
while(null!=cur){
next=cur.next;
cur.next=reverseHead.next;//头删
reverseHead.next=cur;//头插
cur=next;
}
head.next=reverseHead.next;
}
leetCode141:判断链表是否有环
思路:使用快慢指针。快指针走2个位置,慢指针走1个位置,当2个指针重合表示有环
public boolean hasCycle(Node head){
Node slow=head;
Node fast=head;
if(head==null){return false;}
while(null!=fast && null!=fast.next && null!=fast.next.next){
slow=slow.next;
fast=fast.next.next;
if(slow==fast){return true;}
}
return false;
}
双向链表实现增删改
import java.util.Stack;
public class DoubleLinkedListDemo {
public static void main(String[] args) {
HeroNode2 heroNode1 = new HeroNode2("宋江", "及时雨", 1);
HeroNode2 heroNode2 = new HeroNode2("卢俊义", "玉麒麟", 2);
HeroNode2 heroNode3 = new HeroNode2("吴用", "智多星", 3);
HeroNode2 heroNode6 = new HeroNode2("林冲", "豹子头", 6);
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.add(heroNode1);
doubleLinkedList.add(heroNode2);
doubleLinkedList.add(heroNode3);
doubleLinkedList.add(heroNode6);
doubleLinkedList.show();
System.out.println("===========================================");
//修改
HeroNode2 heroNode = new HeroNode2("宋江", "及时雨~", 1);
doubleLinkedList.update(heroNode);
doubleLinkedList.show();
System.out.println("===========================================");
//删除
doubleLinkedList.del(1);
doubleLinkedList.show();
System.out.println("===========================================");
//按照序号添加节点到指定位置
HeroNode2 heroNode5 = new HeroNode2("鲁智深", "花和尚", 5);
doubleLinkedList.addByOrder(heroNode5);
doubleLinkedList.show();
}
}
//定义一个链表类,提供管理节点的方法:添加删除修改查找
class DoubleLinkedList{
//初始化一个头节点,头节点不要动,也不存放数据,编号为0
private HeroNode2 head=new HeroNode2("","",0);
//获取头节点
public HeroNode2 getHead() {
return head;
}
//添加节点到单向链表第一种方式,不考虑编号顺序:将传入的节点放入链表的尾部
public void add(HeroNode2 newNode){
//因为head节点不能动,需要一个辅助游标遍历来寻找到最后一个节点
HeroNode2 tamp=head;
while (true){
if (tamp.next==null){
//说明链表为空或者遍历到最后一个位置
break;
}
//没有到最后一个位置,后移向下遍历
tamp=tamp.next;
}
//除了循环体,tamp为最后一个节点位置,让tamp的next属性指向新的节点,添加成功
tamp.next=newNode;
newNode.pre=tamp;
}
//添加节点到单向链表第二种方式,根据编号的大小将节点添加到指定位置,如果编号已存在表示添加失败
public void addByOrder(HeroNode2 newNode){
//因为head节点不能动,需要一个辅助游标遍历来寻找新的节点的上一个节点位置
HeroNode2 tamp=head;
boolean flag=false;//flag表示添加的编号是否存在 默认不存在
while (true){
if (tamp.next==null){
//此时tamp在链表最后一个位置,新的节点的上一个节点位置位于最后一个位置,要退出循环体添加元素;
break;
}
if (tamp.next.number>newNode.number){
//此时tamp代表找到新节点的上一个节点的位置 退出循环体在tamp的后面添加元素
//例如编号1,4,将2插入进去,需要找到1。因为1.next=4,4>2,所以找到了1,此时tamp=1
break;
}
if (tamp.next.number==newNode.number){
//tamp.next.number:tamp=head一开始tamp是头部,头部编号为0,因此遍历查找从next开始遍历
//比如链表内有1、2,想要添加1,那么tamp=0,tamp.next=1,编号已存在;
//说明编号已经存在
flag=true;
break;
}
//不满足以上条件 后移
tamp=tamp.next;
}
if (flag){
System.out.printf("编号%d已经存在,无法添加",newNode.number);
}else{
//先写新节点的尾部指向
newNode.next=tamp.next;
tamp.next.pre=newNode;
tamp.next=newNode;
newNode.pre=tamp;
}
}
//修改节点的信息 根据no编号来修改,即只能修改name和nickname
public void update(HeroNode2 newheronode){
boolean flag=false;//
if (head.next==null){
System.out.println("链表为空~");
return;
}
//链表不为空说明可以从第一个元素开始遍历 head.next表示第一个元素
HeroNode2 tamp=head.next;
while (true){
if (tamp.number==newheronode.number){
flag=true;
break;
}
if (tamp==null){
//tamp此时表示在链表最后一个元素的后一个空位置 表示链表遍历结束 tamp已经为空了,如果使用tamp.next就会报空指针异常
break;
}
tamp=tamp.next;
}
if (flag){
tamp.name=newheronode.name;
tamp.nickname=newheronode.nickname;
}else{
System.out.println("没有找到该元素");
}
}
//删除节点
//思路:head不能动,需要tamp来找到待删除节点newHeroNode的上一个节点位置,因此tamp一开始是head
//比较时是用 tamp.next.number与newHeroNode.number来进行比较
public void del(int number){
if (head.next==null){
System.out.println("链表为空无法删除");
return;
}
HeroNode2 tamp=head.next;
boolean flag=false;
while (true){
if (tamp==null){ //tamp在最后一个元素位置的next空域
break;
}
if (tamp.number==number){//找到了待删除节点newHeroNode的位置tamp
flag=true;
break;
}
tamp=tamp.next;
}
if (flag){
//这里要注意如果tamp是最后一个元素,只需要断开向右的连接即可,不可能让null再指回去,否则会报出空指针异常
tamp.pre.next=tamp.next;
if (tamp.next!=null){
tamp.next.pre=tamp.pre;}
}else {
System.out.println("没有找到待删除节点");
}
}
//显示链表
public void show(){
if (head.next==null){
System.out.println("链表为空~");
return;
}
HeroNode2 tamp=head.next;
while (true){
if (tamp==null){
//循环终止条件
//说明遍历链表结束 tamp就是最后一个元素的空位置处,不是最后一个元素
break;
}
System.out.println(tamp);
tamp=tamp.next;
}
}
}
//定义HeroNode2类,每个对象是一个节点,一个节点包含数据域和下一个节点next域
class HeroNode2{
public String name;
public String nickname;
public int number;
public HeroNode2 next;//下一个节点
public HeroNode2 pre;//上一个节点
public HeroNode2(String name, String nickname, int number) {
this.name = name;
this.nickname = nickname;
this.number = number;
}
//为了显示方法 重写toString
@Override
public String toString() {
return "HeroNode{" +
"name='" + name + '\'' +
", nickname='" + nickname + '\'' +
", number=" + number +
'}';
}
}
约瑟夫问题
丢手绢出圈
import com.sun.imageio.plugins.common.BogusColorSpace;
public class Josepfu06 {
public static void main(String[] args) {
CircleSingleLinklist circleSingleLinklist = new CircleSingleLinklist();
circleSingleLinklist.AddNode(5);
circleSingleLinklist.showNode();
circleSingleLinklist.countBoy(2,1,5);
}
}
//创建一个环形单向链表
class CircleSingleLinklist{
private Boy first=null;//第一个节点当前没有编号
//添加节点构成单向环形链表
public void AddNode(int nums){
if (nums<1){
System.out.println("输入节点数为0,无法添加");
return;
}
else{
Boy curboy=null;//辅助指针帮助构建环形链表
for (int i = 1; i <= nums; i++) {
Boy boy = new Boy(i);//根据编号创建小孩节点
if (i==1){
first=boy;//第一个节点指向第一个小孩子
first.setNext(first);//first下一个节点指向自己
curboy= first;//让curboy指向第一个小孩
}else{
//从第二个小孩子开始:让curboy从第一个小孩指向下一个小孩
curboy.setNext(boy);
boy.setNext(first);
//curboy=curboy.getNext();
curboy=boy;//curboy向后移为下一个小孩
}
}
}
}
//小孩出圈
public void countBoy(int CountNum,int StartNo,int SumBoy){
/*
* CountNum:报几下出圈
* startNo:第一个报数的人的编号
* Sumboy:总人数
* */
//先对数据进行校验
if (first==null|| StartNo>SumBoy||StartNo<1){
System.out.println("数据错误");
return;
}
//1、构建辅助指针helper,并指向最后一个节点
Boy helper=first;
for (int i = 0; i < SumBoy-1; i++) {
helper=helper.getNext();
}
// 2、移动first与helper,到first指向第一个要报数的人处,helper在first之后,
for (int i = 0; i < StartNo-1; i++) {
first=first.getNext();
helper=helper.getNext();
}
//3、通过报数的大小来移动指针进而出圈
while (true){
if (first==helper){break;}//圈中只有一个节点
//开始报数移动指针到出圈小孩位置
for (int i = 0; i < CountNum - 1; i++) {
first=first.getNext();
helper=helper.getNext();
}
System.out.printf("第%d个小孩出圈\n",first.getNo());
first=first.getNext();//first指向下一个节点
helper.setNext(first);//helper的下一个节点指向新的first
}
System.out.printf("最后第%d个小孩出圈\n",first.getNo());
}
public void showNode(){
if (first==null){
System.out.println("链表为空无法遍历");
return;
}
Boy curboy=first;
while (true){
//此处要思考的是:先打印,再判断。如果先判断再打印,就会让最后一个节点无法打印。
System.out.printf("当前节点编号为%d\n",curboy.getNo());
if (curboy.getNext()==first){break;}
curboy=curboy.getNext();
}
}
}
class Boy{
private int No;
private Boy next;
public Boy(int NO){
this.No=NO;
}
public int getNo() {
return No;
}
public void setNo(int no) {
No = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}