目录
队列在生活中的应用:银行排队办理业务:
一、队列简介:
总结:即增加数据在队列的尾部增加,和生活中的排队一样,而取出数据是在队列的队首。
二、队列的存储结构:
单词说明:
rear:后面,尾部
front:前面,首部
上图中:使用数组模拟队列的存储示意:
假溢出:顺序队列因多次入队列和出队列操作后出现的尚有存储空间但不能进行入队列操作的溢出。
真溢出:顺序队列的最大存储空间已经存满,此时进行入队列操作所引起的溢出(即没有多余的物理空间)。
三、顺序队列的常见实现方式:
方式一:(不推荐)
队头不动,出队列时队头后的所有元素向前移动。这种方式能克服队列的假溢出。
但是这种队列缺陷:操作是如果出队列比较多,要搬移大量元素。
方式二:
队头移动,出队列时队头向后移动一个位置。
但是这种队列缺陷:如果还有新元素F进行入队列容易造成假溢出。
方式二升级版:(推荐)
将数组存储区看成一个首尾相接的环形,然后形成一个环形队列,避免假溢出。
四、队列的顺序存储实现,底层使用数组来模拟队列类:
基础版:不能实现重复使用队列:
代码实现:
代码演示:
package com.fan.queue;
import java.util.Scanner;
//测试类
public class ArrayQueueDemo{
public static void main(String[] args) {
//制作用于菜单
ArrayQueue arrayQueue = new ArrayQueue(3);
char key = ' ';//用于接收用户输入的指令
Scanner scanner = new Scanner(System.in);
boolean loop =true;
while(loop){
System.out.println("s(show):显示队列");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据到队列");
System.out.println("g(get):从队列取出数据");
System.out.println("h(head):查看队列头数据");
System.out.println("请输入指令:");
key = scanner.next().charAt(0);//接收编号指令
switch (key){
case 's':arrayQueue.showQueue();break;
case 'a':
System.out.println("请输入一个数字:");
int value = scanner.nextInt();
arrayQueue.addQueue(value);
break;
case 'g'://取出数据
try {
Object res = arrayQueue.getQueue();
System.out.printf("取出的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'h'://查看队头数据
try {
Object res = arrayQueue.headQueue();
System.out.printf("队首的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'e'://退出
scanner.close();
loop = false;
break;
default:break;
}
}
}
}
//数组模拟队列
class ArrayQueue {
private int maxSize;//表示数组的最大容量
private int front;//队列头的 下标
private int rear;//队列尾 的 下标
private Object[] arr;//该数据用于存放数据,模拟队列
//创建队列的构造器
public ArrayQueue(int arrMaxSize){
maxSize = arrMaxSize;
arr = new Object[maxSize];
front = -1;//指向队列头部,分析出front是指向队列头部的一个位置
rear = -1;//指向队列尾部,指向对垒尾部的数据(即就是队列最后一个数据)
}
//判断队列是否满了,方法名可以自定义,但是此方法不能缺少
public boolean isFull(){
return rear == maxSize - 1;//如果数组容量最大值减一等于队尾下标,则满
}
//判断队列是否为空,方法名可以自定义,但是此方法不能缺少
public boolean isEmpty(){
return rear == front;//当队首下标等于队尾下标的时候,队列为空
}
//添加数据到队列,方法名可以自定义,但是此方法不能缺少
public void addQueue(Object o){
//判断队列是否满了
if(isFull()){
System.out.println("队列满了,不能加入数据--");
return;
}
rear++;//让rear指针后移
arr[rear] = o;//将新元素放入后移的空位置
}
//获取队列的数据,出队操作,方法名可以自定义,但是此方法不能缺少
public Object getQueue(){
//判断队列是否为空
if(isEmpty()){
//通过抛出异常
throw new RuntimeException("队列为空,不能取出数据--");
}
front ++ ;//正常取数据,队首指针后移
return arr[front];//将后移后的指向元素取出来
}
//显示队列的所有数据
public void showQueue(){
//遍历
if(isEmpty()){
System.out.println("队列为空,没有数据--");
return;
}
//遍历
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d]=%d\n",i,arr[i]);
}
}
//显示队列的头数据,注意不是取出数据
public Object headQueue(){
if(isEmpty()){
throw new RuntimeException("队列为空,没有数据");
}
return arr[front + 1];//开始front为-1,加一才到数组的第一个元素
}
}
测试:发现这个队列只能用一次,存满后取出元素后不能重复使用(假溢出),其他功能正常;
升级版:循环数组搭建循环队列:
情况一的思路分析:
代码演示环形数组模拟队列:
package com.fan.queue;
import java.util.Scanner;
//测试类
public class CircleArrayQueueDemo{
public static void main(String[] args) {
//制作用于菜单
CircleArrayQueue circleArrayQueue = new CircleArrayQueue(4);
char key = ' ';//用于接收用户输入的指令
Scanner scanner = new Scanner(System.in);
boolean loop =true;
while(loop){
System.out.println("s(show):显示队列");
System.out.println("e(exit):退出程序");
System.out.println("a(add):添加数据到队列");
System.out.println("g(get):从队列取出数据");
System.out.println("h(head):查看队列头数据");
System.out.println("请输入指令:");
key = scanner.next().charAt(0);//接收编号指令
switch (key){
case 's':circleArrayQueue.showQueue();break;
case 'a':
System.out.println("请输入一个数字:");
int value = scanner.nextInt();
circleArrayQueue.addQueue(value);
break;
case 'g'://取出数据
try {
Object res = circleArrayQueue.getQueue();
System.out.printf("取出的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'h'://查看队头数据
try {
Object res = circleArrayQueue.headQueue();
System.out.printf("队首的数据是%d\n",res);
}catch (Exception e){
System.out.println(e.getMessage());
}
break;
case 'e'://退出
scanner.close();
loop = false;
break;
default:break;
}
}
}
}
//数组模拟队列
class CircleArrayQueue {
private int maxSize;//表示数组的最大容量
private int front;//队列头的 下标,指向下标0
private int rear;//队列尾 的 下标,指向下标0
private Object[] arr;//该数据用于存放数据,模拟队列
//创建队列的构造器
public CircleArrayQueue(int arrMaxSize){
maxSize = arrMaxSize;
arr = new Object[maxSize];
front = 0;//指向队列头部
rear = 0;//指向队列尾部
}
//判断队列是否满了,方法名可以自定义,但是此方法不能缺少
public boolean isFull(){
return (rear + 1) % maxSize == front;//如果数组容量最大值减一等于队尾下标,则满
}
//判断队列是否为空,方法名可以自定义,但是此方法不能缺少
public boolean isEmpty(){
return rear == front;//当队首下标等于队尾下标的时候,队列为空
}
//添加数据到队列,方法名可以自定义,但是此方法不能缺少
public void addQueue(Object o){
//判断队列是否满了
if(isFull()){
System.out.println("队列满了,不能加入数据--");
return;
}
//直接将数据插入
arr[rear] = o;//将新元素放入后移的空位置
//为了让数组的下标在0-3(假设maxSize=4 )之间循环,我们采用取模的策略来防止数组下标越界
rear = (rear + 1) % maxSize;//让rear指针后移
}
//获取队列的数据,出队操作,方法名可以自定义,但是此方法不能缺少
public Object getQueue(){
//判断队列是否为空
if(isEmpty()){
//通过抛出异常
throw new RuntimeException("队列为空,不能取出数据--");
}
Object v = arr[front];//将队首元素取出备用
front = (front + 1) % maxSize ;//队首指针后移
return v;//返回取出的元素
}
//显示队列的所有数据
public void showQueue(){
//遍历
if(isEmpty()){
System.out.println("队列为空,没有数据--");
return;
}
//遍历,size()为实际元素个数
for (int i = front; i < front + size() ; i++) {
System.out.printf("arr[%d]=%d\n",i % maxSize,arr[i % maxSize]);
}
}
//显示队列的头数据,注意不是取出数据
public Object headQueue(){
if(isEmpty()){
throw new RuntimeException("队列为空,没有数据");
}
return arr[front];//
}
//求出当前队列有效元素的个数
public int size(){
return (rear - front + maxSize) % maxSize;
}
}
测试:
总结:禁存区是随着存取元素的环形动态变换的。
使用单链表模拟队列:
package com.fan.queue;
public class LinkedlistQueueDemo {
public static void main(String[] args) {
LinkedlistQueue linkedlistQueue = new LinkedlistQueue();
linkedlistQueue.addQueue(new Person(1,"zhang=san"));
linkedlistQueue.addQueue(new Person(2,"zhang=san"));
linkedlistQueue.addQueue(new Person(3,"zhang=san"));
linkedlistQueue.addQueue(new Person(4,"zhang=san"));
linkedlistQueue.show();
System.out.println("队列元素个数:"+linkedlistQueue.getSize());
System.out.println("出队:"+linkedlistQueue.getPerson());
System.out.println("出队:"+linkedlistQueue.getPerson());
System.out.println("出队:"+linkedlistQueue.getPerson());
System.out.println("出队:"+linkedlistQueue.getPerson());
linkedlistQueue.show();//出队后显示队列
linkedlistQueue.addQueue(new Person(7,"zhang=san"));
linkedlistQueue.addQueue(new Person(8,"zhang=san"));
System.out.println("向元队列再次添加元素后显示:");
linkedlistQueue.show();//向元队列再次添加元素后显示
System.out.println("队首元素:"+ linkedlistQueue.getQueueHead());
}
}
//链表队列类
class LinkedlistQueue{
//设置有头节点的队列,队首
private Person headNode = new Person(-1,"");
private Person tailNode = new Person(-1,"");
//构造方法
public LinkedlistQueue() {
headNode = tailNode;//开始空的链表,首尾重合
}
//入队,采用头插法,遍历的时候从头遍历
public void addQueue(Person newPerson){
Person curr = tailNode;//这里本质上还是链表的头
//如果是第一个节点,直接添加
if(tailNode.getNext() == null){
tailNode.setNext(newPerson);
return;//第一个节点添加完,退出方法
}
newPerson.setNext(curr.getNext());
curr.setNext(newPerson);
}
//出队,相当于从链表的尾部删除元素
public Person getPerson(){
if(tailNode.getNext() == null){
return null;
}
Person curr = tailNode;
while(true){
if(curr.getNext().getNext() == null){
break;
}
curr = curr.getNext();
}
Person v = curr.getNext();//要出队的元素
curr.setNext(null);
return v;
}
public void show(){
Person curr = tailNode.getNext();
if(tailNode.getNext() == null){
System.out.println("队列为空---");
return;
}
while(true){
if(curr == null){
break;
}
System.out.println(curr);
curr = curr.getNext();
}
}
//获取队列元素个数
public int getSize(){
Person curr = tailNode.getNext();
int size = 0;
while(true){
if(curr == null){
break;
}
size++;
curr = curr.getNext();
}
return size;
}
//获取队首元素
public Person getQueueHead(){
Person curr = tailNode;
while(true){
if(curr.getNext() == null){
break;
}
curr = curr.getNext();
}
return curr;
}
}
//队列链表的节点
class Person{
//数据域
private Integer id;
private String name;
//指针域
private Person next;
public Person(Integer id, String name) {
this.id = id;
this.name = name;
}
public Person() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person getNext() {
return next;
}
public void setNext(Person next) {
this.next = next;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
测试结果:
五、链表:
链表介绍:
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
(1)单向链表:
(单链表)是链表的一种,其特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始;链表是使用指针进行构造的列表;又称为结点列表,因为链表是由一个个结点组装起来的;其中每个结点都有指针成员变量指向列表中的下一个结点;
链表是由结点构成,head指针指向第一个成为表头结点,而终止于最后一个指向NULL的指针。
链表只能顺序访问,不能随机访问,链表这种存储方式最大缺点就是容易出现断链,一旦链表中某个节点的指针域数据丢失,那么意味着将无法找到下一个节点,该节点后面的数据将全部丢失
这里我们将head节点理解成数据域为null,指针域是next.
需求一:将新节点添加到链表尾部:
代码演示:
package com.fan.linkedlist;
public class SingleLinkedListDemo{
public static void main(String[] args) {
//创建单独的每个节点
HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星");
HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头");
//创建链表,并拼接单独的节点
SingleLinkedList singleLinkedList = new SingleLinkedList();
singleLinkedList.add(heroNode1);
singleLinkedList.add(heroNode2);
singleLinkedList.add(heroNode3);
singleLinkedList.add(heroNode4);
//显示链表
singleLinkedList.list();
}
}
//定义SingleLinkedList,管理我们的英雄,即组合这个链表
class SingleLinkedList {
//一个单链表,至少包含一个头节点
//(1)先初始化一个头节点,头节点不要动,不存放具体的数据
private HeroNode head = new HeroNode(0,"","");
//(2)添加节点组合到单向链表
//思路:当不考虑编号顺序是
//1.找到当前链表的最后节点
//2.将左后这个节点的next指向新的节点
public void add(HeroNode heroNode){
//重点方法:
//因为head节点不能动,因此我们需要一个辅助遍历节点temp
HeroNode temp = head;//将head节点赋值给临时节点
//遍历链表,找到最后
while(true){
//找到链表最后,即只有一个头节点,temp此时就是head节点
if(temp.next == null){
break;
}
//如果没有找到最后,就将temp后移,类似于i=i+1,那种i用来遍历数组
temp = temp.next;//将下一个位置的节点赋值给temp临时节点
}
//当退出while循环时,temp就指向了链表的最后
//将最后这个节点的next指向新的节点
temp.next = heroNode;
}
//显示链表【遍历】
public void list(){
//判断链表是否为空,只有一个头节点的话直接结束
if(head.next == null){
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此我们们需要一个临时节点变量temp来遍历
HeroNode temp = head.next;//将头节点的下一个节点赋值给临时节点
while(true){
//判断是否到链表最后
if(temp == null){
break;
}
//输出节点信息
System.out.println(temp);
//将temp后移,一定小心
temp = temp.next;
}
}
}
//可以将节点定义成内部类,方便组合使用
//定义HeroNode节点,每个HeroNode对象就是一个节点,属性可以自己定义
class HeroNode{
//no,name,nickname属于数据域,next属于指针域
public Integer no;
public String name;
public String nickname;
//指向下一个节点,下一个节点也是完整的节点对象
public HeroNode next;
//构造器,用于构造一个节点对象
public HeroNode(Integer no,String name,String nickname){
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override //为了显示方法,我们重写tosting
public String toString() {
return "HeroNode{" +"no="+no+
",name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
测试:发现链表添加节点成功:
需求二:按照编号顺序添加指定位置:
增加的代码:
//按照编号顺序添加到链表中,插入顺序乱序
public void addByIdOrder(HeroNode newNode){
//因为头节点不能动,我们需要一个辅助节点变量来帮助我们找到添加位置
//因为是单链表,我们找到的temp是位于添加位置前的一个节点,否则插入不了
HeroNode temp = head;
boolean flag = false;//flag标志添加的编号是否存在,默认不存在
//while是为了遍历找插入位置
while(true){
//只有一个头节点,则退出后,直接添加
if(temp.next == null){
break;
}
//判断编号定插入位置
if(newNode.no < temp.next.no ){//位置找到,就在temp的后面插入
break;
}else if(temp.next.no == newNode.no){//说明希望添加的新节点的编号已经存在
flag = true;//说明编号存在
break;
}
//指针后移,遍历当前链表
temp = temp.next;
} //end-while
//判断flag的值,进行真正插入
if(flag){//flag为true,不能添加,说明编号存在了
System.out.printf("准备插入的节点的标号是%d," +
"已经存在,不能加入\n",newNode.no);
}else{
//插入到链表中,temp的后面
newNode.next = temp.next;//将原来temp的旧的下一级给到新节点的下一级
temp.next = newNode;//将现在的新节点串联到上一级temp的下一级next
}
}
//测试代码:
public class SingleLinkedListDemo{
public static void main(String[] args) {
//创建单独的每个节点
HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星");
HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头");
//创建链表,并拼接单独的节点
SingleLinkedList singleLinkedList = new SingleLinkedList();
/* singleLinkedList.add(heroNode1);
singleLinkedList.add(heroNode2);
singleLinkedList.add(heroNode3);
singleLinkedList.add(heroNode4);*/
singleLinkedList.addByIdOrder(heroNode4);
singleLinkedList.addByIdOrder(heroNode3);
singleLinkedList.addByIdOrder(heroNode2);
singleLinkedList.addByIdOrder(heroNode1);
//显示链表
singleLinkedList.list();
}
}
测试结果:
修改节点信息:
//修改节点的信息,根据no编号来修改,即no编号不能改
//说明,1.根据newHeroNode的no来修改即可,参数是一个新节点
public void update(HeroNode newHeroNode){
//判断是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
HeroNode temp = head.next;
boolean flag = false;//表示是否找到该节点
while(true){//while循环遍历链表
if(temp == null){
break;//已经遍历完链表
}
if(temp.no == newHeroNode.no){
//找到了
flag = true;
break;
}
temp = temp.next;
}
//根据flag判断是否找到要修改的节点
if(flag){//找到并修改信息
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}else{//没有找到
System.out.printf("没有找到编号为%d的节点,不能修改\n",
newHeroNode.no );
}
}
删除指定节点:
//删除指定节点,
/*
* 1.思路:1.head不能动,因此我们需要一个temp辅助节点找到待删除
* 节点的前一个节点
* 2.说明我们在比较时,是temp.next.no和需要删除的节点的no比较
* */
public void del(int no){
HeroNode temp = head;
boolean flag = false;//标志是否找到待删除节点
while(true){
if(temp.next == null){
break;
}
if(temp.next.no == no){
flag = true;//找到了
break;
}
temp = temp.next;//temp后移,遍历
}//end-while
//判断flag
if(flag){//找到
temp.next = temp.next.next;//可以删除
}else{
System.out.printf("要删除的%d节点不存在\n",
no);
}
}
单链表面试题:
package com.fan.linkedlist;
public class SingleLinkedTest {
public static void main(String[] args) {
Node node1 = new Node(1, "Node1");
Node node2 = new Node(2, "Node2");
Node node3 = new Node(3, "Node3");
Node node4 = new Node(4, "Node4");
MyLineked myLineked = new MyLineked();
myLineked.addNode(node1);
myLineked.addNode(node2);
myLineked.addNode(node3);
myLineked.addNode(node4);
System.out.println();
myLineked.listAll();
//问题一:求单链表中有效节点的个数," +
// "遍历的时候统计个数即可
System.out.println("问题一:求单链表中有效节点的个数," +
"遍历的时候统计个数即可:");
int nodeLength = myLineked.getNodeLength(myLineked.getHeadNode());
System.out.println(nodeLength);
//问题二;找单链表中倒数第K个节点
System.out.println("问题二;找单链表中倒数第K个节点");
Node lastIndex = MyLineked.findLastIndex(myLineked.getHeadNode(), 1);
System.out.println(lastIndex);
//测试单链表的反转
System.out.println("单链表的反转:");
MyLineked.reversal(myLineked.getHeadNode());
myLineked.listAll();
}
}
class MyLineked{
//链表设置头节点,没有具体数据域
private Node headNode = new Node(0,"");
public Node getHeadNode() {
return headNode;
}
//增加节点的方法
public void addNode(Node newNode){
Node temp = headNode;
//当只有一个头节点的时候,退出循环,在循环外增加
while(true){
if(temp.next == null){
break;//退出循环
}
temp = temp.next;
}//end-while
temp.next = newNode;//将新节点插入到末尾
}
//显示链表中所有节点信息
public void listAll(){
Node temp = headNode.next;//遍历有效数据,除去头节点
//循环遍历
while(true){
if(temp == null){
break;
}
System.out.println(temp);//打印节点信息
temp = temp.next;//辅助节点后移
}
}
//问题一:求单链表中有效节点的个数,遍历的时候统计个数即可
//参数是头节点,可以用链表来获取头节点
public static int getNodeLength(Node headNode){
int length = 0;
Node curr = headNode.next;//从头节点下一个开始遍历
while(true){
if(curr == null){
break;//遍历结束
}
//当前节点是有效节点
length++;
curr = curr.next;
}//end-while
return length;
}
//问题二;找单链表中倒数第K个节点
//假如有4个节点,则倒数第一个就是正第四个,倒数第二个,就是第三个,即长度+1 -倒数
//参数是链表和倒数的序号
public static Node findLastIndex(Node headNode,int index){
//判断空
if(headNode.next == null){
return null;//没有找到
}
int size = MyLineked.getNodeLength(headNode);//获取链表有效长度,参数头节点
//做一个index的校验
if(index <= 0 || index > size){
return null;
}
Node curr = headNode.next;//让头节点指向临时节点下一个节点,必要的步骤
//注意,这里size-index个次数,是临时指针移动的次数
for (int i = 0; i < size - index; i++) {
curr = curr.next;
}
return curr;
}
//问题三:单链表的反转,我们单链表传入的参数是头节点,因为单链表只能从头遍历
public static void reversal(Node headNode){
//如果当前链表为空,或者只有一个节点,无需反转,直接返回
if(headNode.next == null || headNode.next.next == null){
return;
}
//定义一个辅助节点,帮助我们遍历curr
Node curr= headNode.next;//curr指向第一个有效节点
Node temp = null;//临时节点。指向当前节点【curr】的下一个节点
Node reverseHeadNode = new Node(0,"");//新建头节点
//遍历原来的链表,每遍历一个,就将其取出,并放在新的链表
//reverseHeadNode的最前端
while(curr != null){// 当前结点为null,说明位于尾结点
//第①步
temp = curr.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
//让curr的后驱节点变成reverseHeadNode的下一个节点,
//第一次的时候reverseHeadNode的下一个节点是null,即指向null,即单链表的结尾处
//第②步,将curr的后驱节点串好
curr.next = reverseHeadNode.next;
//第③步,将curr当前串到前面
reverseHeadNode.next = curr;//让curr成为reverseHeadNode的后驱节点
//第④步
curr = temp;//让curr后移
}
//将headNode.next指向reverseHeadNode.next ,实现单链表的反转
headNode.next = reverseHeadNode.next;
}
}
//定义一个节点类
class Node{
//数据域id,name
Integer id;
String name;
Node next;//指针域
public Node(Integer id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Node{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
单链表的反转:
单链表遍历反转原理图:(配合代码看)
package com.fan.linkedlist;
public class SingleLinkedTest {
public static void main(String[] args) {
Node node1 = new Node(1, "Node1");
Node node2 = new Node(2, "Node2");
Node node3 = new Node(3, "Node3");
Node node4 = new Node(4, "Node4");
MyLineked myLineked = new MyLineked();
myLineked.addNode(node1);
myLineked.addNode(node2);
myLineked.addNode(node3);
myLineked.addNode(node4);
System.out.println();
myLineked.listAll();
//问题一:求单链表中有效节点的个数," +
// "遍历的时候统计个数即可
System.out.println("问题一:求单链表中有效节点的个数," +
"遍历的时候统计个数即可:");
int nodeLength = myLineked.getNodeLength(myLineked.getHeadNode());
System.out.println(nodeLength);
//问题二;找单链表中倒数第K个节点
System.out.println("问题二;找单链表中倒数第K个节点");
Node lastIndex = MyLineked.findLastIndex(myLineked.getHeadNode(), 1);
System.out.println(lastIndex);
//测试单链表的反转
System.out.println("单链表的反转:");
MyLineked.reversal(myLineked.getHeadNode());
myLineked.listAll();
}
}
class MyLineked{
//链表设置头节点,没有具体数据域
private Node headNode = new Node(0,"");
public Node getHeadNode() {
return headNode;
}
//增加节点的方法
public void addNode(Node newNode){
Node temp = headNode;
//当只有一个头节点的时候,退出循环,在循环外增加
while(true){
if(temp.next == null){
break;//退出循环
}
temp = temp.next;
}//end-while
temp.next = newNode;//将新节点插入到末尾
}
//显示链表中所有节点信息
public void listAll(){
Node temp = headNode.next;//遍历有效数据,除去头节点
//循环遍历
while(true){
if(temp == null){
break;
}
System.out.println(temp);//打印节点信息
temp = temp.next;//辅助节点后移
}
}
//问题一:求单链表中有效节点的个数,遍历的时候统计个数即可
//参数是头节点,可以用链表来获取头节点
public static int getNodeLength(Node headNode){
int length = 0;
Node curr = headNode.next;//从头节点下一个开始遍历
while(true){
if(curr == null){
break;//遍历结束
}
//当前节点是有效节点
length++;
curr = curr.next;
}//end-while
return length;
}
//问题二;找单链表中倒数第K个节点
//假如有4个节点,则倒数第一个就是正第四个,倒数第二个,就是第三个,即长度+1 -倒数
//参数是链表和倒数的序号
public static Node findLastIndex(Node headNode,int index){
//判断空
if(headNode.next == null){
return null;//没有找到
}
int size = MyLineked.getNodeLength(headNode);//获取链表有效长度,参数头节点
//做一个index的校验
if(index <= 0 || index > size){
return null;
}
Node curr = headNode.next;//让头节点指向临时节点下一个节点,必要的步骤
//注意,这里size-index个次数,是临时指针移动的次数
for (int i = 0; i < size - index; i++) {
curr = curr.next;
}
return curr;
}
//问题三:单链表的反转,我们单链表传入的参数是头节点,因为单链表只能从头遍历
public static void reversal(Node headNode){
//如果当前链表为空,或者只有一个节点,无需反转,直接返回
if(headNode.next == null || headNode.next.next == null){
return;
}
//定义一个辅助节点,帮助我们遍历curr
Node curr= headNode.next;//curr指向第一个有效节点
Node temp = null;//临时节点。指向当前节点【curr】的下一个节点
Node reverseHeadNode = new Node(0,"");//新建头节点
//遍历原来的链表,每遍历一个,就将其取出,并放在新的链表
//reverseHeadNode的最前端
while(curr != null){// 当前结点为null,说明位于尾结点
//第①步
temp = curr.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
//让curr的后驱节点变成reverseHeadNode的下一个节点,
//第一次的时候reverseHeadNode的下一个节点是null,即指向null,即单链表的结尾处
//第②步,将curr的后驱节点串好
curr.next = reverseHeadNode.next;
//第③步,将curr当前串到前面
reverseHeadNode.next = curr;//让curr成为reverseHeadNode的后驱节点
//第④步
curr = temp;//让curr后移
}
//将headNode.next指向reverseHeadNode.next ,实现单链表的反转
headNode.next = reverseHeadNode.next;
}
}
//定义一个节点类
class Node{
//数据域id,name
Integer id;
String name;
Node next;//指针域
public Node(Integer id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Node{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
栈的简单使用:
(2)双向链表:
双向链表的节点增加:
代码实现添加:
package com.fan.linkedlist;
public class DoubLinkedListTest {
public static void main(String[] args) {
Node2 node1 = new Node2(1, "tom1", "tom1");
Node2 node2 = new Node2(2, "tom2", "tom2");
Node2 node3 = new Node2(3, "tom3", "tom3");
DoubLinkedList doubLinkedList = new DoubLinkedList();
doubLinkedList.add(node1);
doubLinkedList.add(node2);
doubLinkedList.add(node3);
doubLinkedList.showNodes(doubLinkedList.getHeadNode());
}
}
class DoubLinkedList{
//设置双向链表的头部,数据域为空
Node2 headNode = new Node2(0,"","");
public Node2 getHeadNode() {
return headNode;
}
//方法二:双向链表的显示,传一个头节点
public void showNodes(Node2 headNode ){
//因为headNode节点不能移动,需要一个辅助节点current去遍历
Node2 current = headNode.next;//从有效节点开始显示
while(true){
//找到链表的最后
if(current == null){//从前往后遍历找
break;
}
System.out.println(current);
//节点后移
current = current.next;
}
}
//方法一:双向链表的添加新节点newNode
public void add(Node2 newNode2){
Node2 current = headNode;
while(true){
if(current.next == null){
break;
}
//继续往后找,如果没有找到,就指针后移
current = current.next;
}
//当出了循环后,就证明找到了双链表的结尾,则开始前后对接
//当退出while循环的时候,current就指向了链表的最后,形成双向链表
current.next = newNode2;
newNode2.pre = current;
}
}
//定义节点类
class Node2{
//id,name,nickname是数据域,pre,next是指针域
int id;
String name;
String nickname;
Node2 pre;//指向上一个节点,默认为null
Node2 next;//指向下一个节点,默认为null
public Node2(int id, String name, String nickname) {
this.id = id;
this.name = name;
this.nickname = nickname;
}
@Override//为了显示方法,我们重写tostring
public String toString() {
return "Node2{" +
"id=" + id +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
双向链表的删除:
删除:注意删除的如果是最后一个节点:
package com.fan.linkedlist;
public class DoubLinkedListTest {
public static void main(String[] args) {
Node2 node1 = new Node2(1, "tom1", "tom1");
Node2 node2 = new Node2(2, "tom2", "tom2");
Node2 node3 = new Node2(3, "tom3", "tom3");
DoubLinkedList doubLinkedList = new DoubLinkedList();
doubLinkedList.add(node1);
doubLinkedList.add(node2);
doubLinkedList.add(node3);
doubLinkedList.showNodes(doubLinkedList.getHeadNode());
System.out.println("删除节点----");
doubLinkedList.del(1);
doubLinkedList.showNodes(doubLinkedList.getHeadNode());
}
}
class DoubLinkedList{
//设置双向链表的头部,数据域为空
Node2 headNode = new Node2(0,"","");
public Node2 getHeadNode() {
return headNode;
}
//方法二:双向链表的显示,传一个头节点
public void showNodes(Node2 headNode ){
//因为headNode节点不能移动,需要一个辅助节点current去遍历
Node2 current = headNode.next;//从有效节点开始显示
while(true){
//找到链表的最后
if(current == null){//从前往后遍历找
break;
}
System.out.println(current);
//节点后移
current = current.next;
}
}
//方法一:双向链表的添加新节点newNode
public void add(Node2 newNode2){
Node2 current = headNode;
while(true){
if(current.next == null){
break;
}
//继续往后找,如果没有找到,就指针后移
current = current.next;
}
//当出了循环后,就证明找到了双链表的结尾,则开始前后对接
//当退出while循环的时候,current就指向了链表的最后,形成双向链表
current.next = newNode2;
newNode2.pre = current;
}
//方法三:双向链表删除()根据id删除
public void del(int id){
//判断当前链表是否为空
if(headNode.next == null){
System.out.println("链表为空,无法删除");
return;
}
Node2 current = headNode.next;//辅助变量(指针)
boolean flag = false;
while(true){
if(current == null){//已经找到链表的结尾,此时flag为false
break;
}
if(current.id == id){
flag = true;//找到了要删除的节点
break;
}
current = current.next;
}
//出了while循环后且flag为true时,
// current指针刚好移动到要删除的位置节点
if(flag){//找到了
current.pre.next=current.next;
//如果是最后一个节点current.next就是null,
//current.next.pre就会报异常
if(current.next !=null){
current.next.pre = current.pre;
}
}else{
System.out.printf("要删除的%d节点不存在\n",id);
}
}
}
//定义节点类
class Node2{
//id,name,nickname是数据域,pre,next是指针域
int id;
String name;
String nickname;
Node2 pre;//指向上一个节点,默认为null
Node2 next;//指向下一个节点,默认为null
public Node2(int id, String name, String nickname) {
this.id = id;
this.name = name;
this.nickname = nickname;
}
@Override//为了显示方法,我们重写tostring
public String toString() {
return "Node2{" +
"id=" + id +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
双向链表的修改:
修改双链表的方法和单链表几乎一样:
package com.fan.linkedlist;
public class DoubLinkedListTest {
public static void main(String[] args) {
Node2 node1 = new Node2(1, "tom1", "tom1");
Node2 node2 = new Node2(2, "tom2", "tom2");
Node2 node3 = new Node2(3, "tom3", "tom3");
DoubLinkedList doubLinkedList = new DoubLinkedList();
doubLinkedList.add(node1);
doubLinkedList.add(node2);
doubLinkedList.add(node3);
doubLinkedList.showNodes(doubLinkedList.getHeadNode());
System.out.println("删除节点----");
doubLinkedList.del(1);
doubLinkedList.showNodes(doubLinkedList.getHeadNode());
System.out.println("修改节点----");
doubLinkedList.update(new Node2(3,"xxx","xxx"));
doubLinkedList.showNodes(doubLinkedList.getHeadNode());
}
}
class DoubLinkedList{
//设置双向链表的头部,数据域为空
Node2 headNode = new Node2(0,"","");
public Node2 getHeadNode() {
return headNode;
}
//方法二:双向链表的显示,传一个头节点
public void showNodes(Node2 headNode ){
//因为headNode节点不能移动,需要一个辅助节点current去遍历
Node2 current = headNode.next;//从有效节点开始显示
while(true){
//找到链表的最后
if(current == null){//从前往后遍历找
break;
}
System.out.println(current);
//节点后移
current = current.next;
}
}
//方法一:双向链表的添加新节点newNode
public void add(Node2 newNode2){
Node2 current = headNode;
while(true){
if(current.next == null){
break;
}
//继续往后找,如果没有找到,就指针后移
current = current.next;
}
//当出了循环后,就证明找到了双链表的结尾,则开始前后对接
//当退出while循环的时候,current就指向了链表的最后,形成双向链表
current.next = newNode2;
newNode2.pre = current;
}
//方法三:双向链表删除()根据id删除
public void del(int id){
//判断当前链表是否为空
if(headNode.next == null){
System.out.println("链表为空,无法删除");
return;
}
Node2 current = headNode.next;//辅助变量(指针)
boolean flag = false;
while(true){
if(current == null){//已经找到链表的结尾,此时flag为false
break;
}
if(current.id == id){
flag = true;//找到了要删除的节点
break;
}
current = current.next;
}
//出了while循环后且flag为true时,
// current指针刚好移动到要删除的位置节点
if(flag){//找到了
current.pre.next=current.next;
//如果是最后一个节点current.next就是null,
//current.next.pre就会报异常
if(current.next !=null){
current.next.pre = current.pre;
}
}else{
System.out.printf("要删除的%d节点不存在\n",id);
}
}
//修改
public void update(Node2 newNode){
//判断是否为空
if(headNode.next ==null){
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根基编号id找,并定义一个辅助变量curr
Node2 curr= headNode.next;
boolean flag =false;
while(true){
if(curr == null){
break;//已经遍历完链表
}
if(curr.id == newNode.id){
flag = true;
break;
}
curr = curr.next;
}
if(flag){//找到了要修改的节点
//修改数据域
curr.name = newNode.name;
curr.nickname = newNode.nickname;
}else{//没有找到
System.out.printf("没有找到标号为%d的节点,不能修改\n",newNode.id);
}
}
}
//定义节点类
class Node2{
//id,name,nickname是数据域,pre,next是指针域
int id;
String name;
String nickname;
Node2 pre;//指向上一个节点,默认为null
Node2 next;//指向下一个节点,默认为null
public Node2(int id, String name, String nickname) {
this.id = id;
this.name = name;
this.nickname = nickname;
}
@Override//为了显示方法,我们重写tostring
public String toString() {
return "Node2{" +
"id=" + id +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
双向链表按编号顺序插入节点:
练习题代码:
package com.fan.linkedlist;
public class DoubLinkedListTest2 {
public static void main(String[] args) {
HeroNode3 node1 = new HeroNode3(4, "dd", "dd");
HeroNode3 node2 = new HeroNode3(2, "bb", "bb");
HeroNode3 node3 = new HeroNode3(1, "aa", "aa");
HeroNode3 node4 = new HeroNode3(3, "cc", "cc");
DoubleLinkedListHero doubleLinkedListHero = new DoubleLinkedListHero();
doubleLinkedListHero.addByOrder(node1);
doubleLinkedListHero.addByOrder(node2);
doubleLinkedListHero.addByOrder(node3);
doubleLinkedListHero.addByOrder(node4);
doubleLinkedListHero.showNodes(doubleLinkedListHero.getHeadNode());
System.out.println("-------------");
HeroNode3 node5 = new HeroNode3(-2, "88", "88");
doubleLinkedListHero.addByOrder(node5);
doubleLinkedListHero.showNodes(doubleLinkedListHero.getHeadNode());
}
}
//双向链表类
class DoubleLinkedListHero{
//声明一个链表的头节点,没有数据域的
HeroNode3 headNode = new HeroNode3(0,"","");
public HeroNode3 getHeadNode() {
return headNode;
}
//显示链表节点,参数是一个头节点
public void showNodes(HeroNode3 headNode){
//因为头节点不能动,需要辅助变量curr,指针变量curr
HeroNode3 curr = headNode.next;
while(true){
if(curr == null){//遍历到链表尾部
break;
}
//显示打印当前节点信息
System.out.println(curr);
curr = curr.next;
}
}
//节点的增加:按照编号顺序增加,(即添加无序,显示有序)
//参数是头节点,因为要遍历找位置插入
public void addByOrder(HeroNode3 newNode){
HeroNode3 curr = headNode;
//flag标志添加的编号是否存在,默认不存在
boolean flag = false;//当需要也能添加相同的编号的时候,去掉flag相关的代码即可
//当退出循环的时候就是要添加的位置
while(true){
if(curr.next == null){//只有一个头节点或者末尾,直接添加
break;
}
if(newNode.no < curr.next.no){//找到了
break;
}else if(curr.next.no == newNode.no){
flag = true;
break;
}
curr = curr.next;
}//end-while
//开始添加
if(flag){
System.out.println("准备插入的编号已经存在,不能插入");
}else{
if(curr.next == null){//添加到末尾,单独处理
curr.next = newNode;
newNode.pre = curr;
}else{
//此处是在curr和temp.next中间添加数据
newNode.next=curr.next;
curr.next.pre = newNode;
curr.next = newNode;
newNode.pre = curr;
}
}
}
}
//定义一个节点类
class HeroNode3{
//数据域
Integer no;//编号
String name;
String nickname;
//指针域
HeroNode3 pre;
HeroNode3 next;
public HeroNode3(Integer no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
@Override
public String toString() {
return "HeroNode3{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
(3)单向环形链表:
为什么实际应用要采用环形队列:
因为环形队列的特点是,不需要进行动态的内存释放和分配,使用固定大小的内存空间反复使用。
非常的简单和高效
单链表实现队列:
定义队列接口:
package com.fan.queue;
/*Java5引入参数化类型(Parameterized Type)的概念,
也称为泛型(Generic)。泛型:就是允许在定义类、接口、
方法时使用类型形参。这个类型形参将在声明变量、创建对象、
调用方法时动态指定,即传入实际的类型参数(也叫传入类型实参)。
传入的类型实参的类型必须是引用类型。*/
// 泛型类:定义类的时候指定类型形参E,在类里面E就可以当成类型使用
public interface Queue<E> {
//入队操作offer方法,参数 E 元素,将来传什么类型就用什么类型
void offer(E element);
//出队操作poll方法,参数 E 元素,将来传什么类型就用什么类型
E poll();//返回出队的元素
//查看队首元素
E peek();//返回队首元素
//判断队列是否为空
boolean isEmpty();
}
定义链表实现队列:
package com.fan.queue;
//
public class LinekdLIstQueue<E> implements Queue<E>{
//作为队列需要有队首和队尾,则定义两个节点
private Node<E> headNode;//保存首节点,开始为null
private Node<E> tailNode;//保存尾节点
@Override//入队方法,参数是一个元素
public void offer(E element) {
//思路,在添加的时候tailNode一直在往队尾移动,一直指向新添加的节点
//1.将元素element封装成节点对象
Node<E> newNode = new Node<>(element);
//2.处理链表为null的情况
if(headNode == null){
//把新节点newNode节点设置为链表的首节点
headNode = newNode;
}else{//3.处理链表不为null的情况
//把新节点newNode添加到尾节点的后面
tailNode.next = newNode;//即队尾添加元素,tailNode是null??
}
//4.把新节点设置为链表的尾节点,不然 tailNode.next就空指针异常
tailNode = newNode;
}
@Override//出栈操作,删除元素,从链表头部删除
public E poll() {
//1.如果对垒为空,则返回null
if(isEmpty()){
return null;
}
//2.定义一个变量,用于保存出栈元素的节点
E e = headNode.item;//出队列的首节点元素
/*//3.定义一个变量,用于保存首节点的后一个节点
Node<E> nextNode = headNode.next;
//4.取消headNode和nextNode之间的连线
headNode.next = null;*/
//5.设置nextNode为链表的首节点
headNode = headNode.next;
return e;//返回出栈的元素
}
@Override
public E peek() {
//1.如果队列为空,则返回null
if(isEmpty()){
return null;
}
//执行到此处,证明队列不为空,返回队首元素
return headNode.item;
}
@Override
public boolean isEmpty() {
return headNode == null;
}
//显示链表所有节点
public void show(){
Node<E> curr = headNode;
//System.out.println(curr);
while(true){
if(curr == null){
break;
}
System.out.println(curr);
curr = curr.next;
}
}
//节点类,我们这里是静态内部类
private static class Node<E>{
//数据域,用于存储节点中的数据
private E item;
//指针域,用于存储指向下一个节点的地址值
private Node<E> next;
public Node(E item) {//为item做初始化的构造方法
this.item = item;
}
public Node(E item, Node<E> next) {//为item和next初始化的构造方法
this.item = item;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
}
测试类:
package com.fan.queue;
public class LinekdLIstQueueTest1 {
public static void main(String[] args) {
LinekdLIstQueue<Integer> linekdLIstQueue = new LinekdLIstQueue<Integer>();
linekdLIstQueue.offer(1);
linekdLIstQueue.offer(2);
linekdLIstQueue.offer(3);
linekdLIstQueue.show();
System.out.println("队首元素:"+linekdLIstQueue.peek());
System.out.println(linekdLIstQueue.poll());
System.out.println(linekdLIstQueue.poll());
System.out.println(linekdLIstQueue.poll());
}
}
测试结果:
黑马的代码: