0.链表介绍
链表是有序的列表,但是他在内存中是这样存储的:
注:
[1].链表是以节点的方式来存储的,是链式存储。
[2].每个节点包含data域,next域:指向下一个节点。
[3].如图发现,链表的的各个节点不一定是连续存储。
[4].链表分带有头结点的链表和没有头结点的链表,根据实际的需求来确定。
1.单链表
单链表(带有头节点)逻辑结构示意图如下:
1.1单链表操作实现:
添加
1.先创建一个head头节点,作用就是指向单链表的头。
2.后面我们每添加一个节点,就直接添加到链表的最后。
遍历
1.通过一个辅助指针变量,帮助我们遍历整个链表。
按照编号顺序添加
1.首先找到新添加节点的位置,是通过辅助变量,通过遍历找到要添加位置的前一个结点。
2.新的节点.next = temp.next。
3.temp.next = 新的节点。
从单链表中删除一个节点的思路
1.我们要先找到需要删除的这个节点的前一个节点temp。
2.temp.next = temp.next.next。
3.被删除的这个节点,将不会有其他引用指向,会被垃圾回收机制回收。
修改
1.通过辅助指针遍历找到要修改的节点。
2.修改节点相应信息。
1.2单链表的应用实例
//链表节点元素类
public class Fund {
/**
* 基金编号
*/
private int no;
/**
* 基金名
*/
private String name;
/**
* 基金经理
*/
private String manager;
/**
* 每一分额价格
*/
private double price;
/**
* 后继结点
*/
private Fund next;
/**
* 构造器
* @param no
* @param name
* @param manager
* @param price
*/
public Fund(int no,String name,String manager,double price) {
this.no = no;
this.name = name;
this.manager = manager;
this.price = price;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getManager() {
return manager;
}
public void setManager(String manager) {
this.manager = manager;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public Fund getNext() {
return next;
}
public void setNext(Fund next) {
this.next = next;
}
@Override
public String toString() {
return "Fund [no=" + no + ", name=" + name + ", manager=" + manager + ", price=" + price + "]";
}
}
单链表
public class SingleLinkedList{
/**
* 头结点
*/
private Fund head = new Fund(0, "", "", 0);
public Fund getHead() {
return head;
}
public void setHead(Fund head) {
this.head = head;
}
/**
* 判断链表是否为空
* @return
*/
public boolean isEmpty() {
return head.getNext() == null;
}
/**
* 遍历链表
*/
public void show() {
//定义辅助指针
Fund temp = head.getNext();
while(temp != null) {
System.out.println(temp.toString());
temp = temp.getNext();//指针后移
}
}
/**
* 添加新的结点到链表尾部
* @param fund
*/
public void add(Fund fund) {
//找到链表的尾结点
Fund temp = head;
while(temp.getNext() != null) {
temp = temp.getNext();
}
//循环结束后,temp的后继为空,即是temp为尾结点
temp.setNext(fund);
}
/**
* 按照基金编号从小到大的顺序添加新的结点到链表中
* @param fund
*/
public void addByNo(Fund fund) {
/*
* 思路:遍历链表找到要插入位置的前一个结点
* 辅助指针最初需要指向头结点,遍历到倒数第二个结点即可
* 如果no值重复,return,如果找到插入位置的前一个位置,break
* 否则指针后移
*
* 最后插入到辅助结点temp后边
*/
System.out.println(fund.toString());
Fund temp = head;
while(temp.getNext() != null) {
if(temp.getNext().getNo() == fund.getNo()) {
System.out.println("该编号的基金信息已存在,无法重复插入!");
return;
}
else if(temp.getNext().getNo() > fund.getNo()) {
break;
}
else {
temp = temp.getNext();
}
}
//循环结束后,如果没有return,直接将结点插入到链表的尾结点
fund.setNext(temp.getNext());
temp.setNext(fund);
}
/**
* 更新结点
* @param fund
*/
public void update(Fund fund) {
/*
* 思路:按照编号找到要修改的基金结点,如果找到修改信息
* 找不到结束
*/
//链表为空时,直接返回return
if(isEmpty()) {
System.out.println("链表为空,无法修改!");
return;
}
//遍历链表找到要删除的结点,丛链表的第一个结点开始
Fund temp = head.getNext();
while(temp != null) {
//如果找到结点,更新信息
if(temp.getNo() == fund.getNo()) {
temp.setName(fund.getName());
temp.setManager(fund.getManager());
temp.setPrice(fund.getPrice());
return;
}//找不到就继续后移指针
else {
temp = temp.getNext();
}
}
System.out.println("没有找到基金编号为:"+fund.getNo()+"的基金,无法修改!");
}
/**
* 通过编号删除结点并返回
* @param no
* @return
*/
public Fund deleteByNo(int no) {
/*
* 思路:遍历链表找到要删除结点的前一个结点
* 如果找不到返回null,找到删除结点并返回
* 方法:从链表的头结点开始,如果temp.next.no == no表示找到
* 使用boolean类型的flag变量标志是否找到要删除结点
*/
if(isEmpty()) {
System.out.println("链表为空,无法删除!");
return null;
}
//定义辅助结点指向头结点
Fund temp = head;
boolean flag = false;
while(temp.getNext() != null) {
if(temp.getNext().getNo() == no) {
flag = true;
break;
}
else {
temp = temp.getNext();
}
}
//如果找到,执行删除操作并返回结点
if(flag) {
Fund fund = temp.getNext();//要删除的结点
temp.setNext(fund.getNext());
return fund;
}//否则返回null
else {
System.out.println("没有找到基金编号为:"+no+"的基金,无法删除!");
return null;
}
}
}
练习题:
1.获得链表有效节点个数
/**
* 获得有效结点个数
* @return
*/
public int getLength() {
//如果链表长度为0,返回0
if(isEmpty()) {
return 0;
}
//否则从第一个结点遍历链表
int length = 0;//记录结点个数
Fund temp = head.getNext();
while(temp != null) {
length++;
temp = temp.getNext();
}
return length;
}
2.查找链表中的倒数第k个节点
/**
* 查找链表的倒数第k个结点
* @param k
* @return
*/
public Fund getReverseOfK(int k) {
/*
* 思路:1.如果链表为空,返回null
* 2.获得链表有效结点个数并验证k值是否合法
* 3.定义辅助指针指向第一个结点,然后让指针后移length-k次即可找到
*/
//1.链表为空
if(isEmpty()) {
System.out.println("链表为空,无法查找!");
return null;
}
//2.k值不合法
int length = getLength();
if(k <= 0 || k > length){
System.out.println("k值不合法!!");
return null;
}
//3.辅助指针指向第一个有效结点
//指针后移length - k次
Fund temp = head.getNext();
for(int i = 0; i < length - k; i++) {
temp = temp.getNext();//指针后移
}
//循环结束后,temp指向的结点就是要找的倒数第k个结点
return temp;
}
3.单链表的反转
/**
* 反转链表
*/
public void reverse(){
//1.如果链表为空或者长度为1,直接返回
if(isEmpty() || getLength() == 1){
return;
}
//2.如果链表长度大于1
//定义一个新的头结点
Fund newHead = new Fund(0,"","",1.12);
//遍历链表,将结点逐个出入到新链表的第一个结点
Fund temp = head.getNext();
while(temp != null){
//记录辅助结点的后一个结点
Fund tempNext = temp.getNext();
//插入到新的链表
temp.setNext(newHead.getNext());
newHead.setNext(temp);
//辅助指针后移
temp = tempNext;
}
//循环结束后,新的链表就是原链表的反转
//老的头结点指向新的头结点即可
head.setNext(newHead.getNext());
}
4.从未到头打印链表,不改变链表的结构
使用栈,遍历链表入栈,然后逐个出栈。
5.合并两个有序的单链表,合并之后的链表依然有序
/**
* 合并两个递增的有序链表
* @param s1
* @param s2
* @return
*/
public static SingleLinkedList merge(SingleLinkedList s1,SingleLinkedList s2){
//1.两个链表有一个为空时,返回另一个
if(s1.isEmpty()){
return s2;
}
else if(s2.isEmpty()){
return s1;
}
//2.将链表1结点逐个插入到链表2
//辅助指针指向表1的第一个有效结点
Fund temp = s1.getHead().getNext();
//当temp不为空时,插入到表2
while(temp != null){
//记录temp的后继
Fund tempNext = temp.getNext();
s2.addByNo(temp);
temp = tempNext;//辅助指针后移
}
//循环结束时,s1中的所有结点都被遍历了一次,并且已经加入到了s2中
//返回链表s2即可
return s2;
}
2.双向链表
管理单向链表的缺点分析:
1.单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
2.单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点。
2.1双链表操作
添加:默认添加到双链表的最后
1.先找到双向链表的最后这个节点
2.temp.next = newNode;
newNode.pre = temp;
修改
和单链表修改操作思路一致
删除
1.因为是双向链表,因此,我们可以实现自我删除某个节点
2.直接找到要删除的节点
3.temp.pre.next = temp.next
4.temp.next.pre = temp.pre
遍历
方法和单链表一样,但是双链表可以正向遍历也可以反向遍历。
2.2双链表实例
public class Fund {
/**
* 基金编号
*/
private int no;
/**
* 基金名
*/
private String name;
/**
* 基金经理
*/
private String manager;
/**
* 每一分额价格
*/
private double price;
/**
* 后继结点
*/
private Fund next;
/**
* 前驱结点
*/
private Fund prior;
/**
* 构造器
* @param no
* @param name
* @param manager
* @param price
*/
public Fund(int no, String name, String manager, double price) {
this.no = no;
this.name = name;
this.manager = manager;
this.price = price;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getManager() {
return manager;
}
public void setManager(String manager) {
this.manager = manager;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public Fund getNext() {
return next;
}
public void setNext(Fund next) {
this.next = next;
}
public Fund getPrior() {
return prior;
}
public void setPrior(Fund prior) {
this.prior = prior;
}
@Override
public String toString() {
return "Fund [no=" + no + ", name=" + name + ", manager=" + manager + ", price=" + price + "]";
}
}
public class DoubleLinkedList {
/**
* 头结点
*/
private Fund head = new Fund(0,"","",0);
/**
* 判断链表是否为空
* @return
*/
public boolean isEmpty(){
return head.getNext() == null;
}
/**
* 打印链表
*/
public void show(){
if(isEmpty()){
System.out.println("链表为空!!!");
return;
}
//定义辅助结点指向第一个有效结点
Fund temp = head.getNext();
while(temp != null){
System.out.println(temp.toString());
temp = temp.getNext();
}
}
/**
* 添加新的结点到链表尾部
* @param fund
*/
public void add(Fund fund){
//找到链表的尾结点
Fund temp = head;
while(temp.getNext() != null){
temp = temp.getNext();
}
//插入
temp.setNext(fund);
fund.setPrior(temp);
}
/**
* 按照基金标号从小到大的顺序添加
* @param fund
*/
public void addByNo(Fund fund){
/*
思路:需要找到待插入结点的前一个结点
1.辅助指针temp从头结点开始
2.如果temp.next!=null执行循环
3.如果temp.next.no == fund.no 方法执行结束,提示该编号的基金已存在
4.需要一个boolean变量表示该节点是否已经插入
5.循环结束后如果flag=false,需要插入到最后
*/
Fund temp = head;
boolean flag = false;
while(temp.getNext() != null){
if(temp.getNext().getNo() == fund.getNo()){
System.out.println("该基金信息已经存在,无法重复添加");
return;
}
else if(temp.getNext().getNo() > fund.getNo()){
flag = true;
//执行插入
fund.setNext(temp.getNext());
temp.getNext().setPrior(fund);
fund.setPrior(temp);
temp.setNext(fund);
break;
}
else{
temp = temp.getNext();
}
}
//插入到最后
if(!flag){
temp.setNext(fund);
fund.setPrior(temp);
}
}
/**
*修改结点信息
* @param fund
*/
public void update(Fund fund) {
/*
* 思路:按照编号找到要修改的基金结点,如果找到修改信息
* 找不到结束
*/
//链表为空时,直接返回return
if(isEmpty()) {
System.out.println("链表为空,无法修改!");
return;
}
//遍历链表找到要删除的结点,丛链表的第一个结点开始
Fund temp = head.getNext();
while(temp != null) {
//如果找到结点,更新信息
if(temp.getNo() == fund.getNo()) {
temp.setName(fund.getName());
temp.setManager(fund.getManager());
temp.setPrice(fund.getPrice());
return;
}//找不到就继续后移指针
else {
temp = temp.getNext();
}
}
System.out.println("没有找到基金编号为:"+fund.getNo()+"的基金,无法修改!");
}
/**
* 按照编号删除结点并返回
* @param fund
* @return
*/
public Fund deleteByNo(Fund fund){
if(isEmpty()){
System.out.println("链表为空,无法删除");
return null;
}
//遍历链表找到要删除的结点
Fund temp = head.getNext();
while(temp != null){
//如果找到就删除
if(temp.getNo() == fund.getNo()){
temp.getPrior().setNext(temp.getNext());
//如果不是尾结点才需要执行这条语句
if(temp.getNext() != null){
temp.getNext().setPrior(temp.getPrior());
}
return temp;//返回删除的结点
}
//否则辅助指针后移
else{
temp = temp.getNext();
}
}
System.out.println("没有找到要删除的结点信息!");
return null;
}
}
3.环形链表与约瑟夫问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
3.1操作
构建环形链表
1.先创建第一个节点,让first指向该节点,并形成环:first.next = first
2.后面当我们每创建一个新的节点,就把该节点加入到已有的环形链表即可。
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) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
@Override
public String toString() {
return "Boy{" +
"no=" + no +
'}';
}
}
/**
* 第一个结点
*/
private Boy first = null;
/**
* 添加孩子结点nums个
* @param nums
*/
public void add(int nums){
//验证nums是否合法
if(nums <= 0){
System.out.println("nums非法!");
return;
}
//添加第一个结点
if(first == null){
Boy boy = new Boy(1);
//单个结点构成环
first = boy;
boy.setNext(first);
}
//使用for循环添加另外nums-1和结点
//因为first不能动,所以定义一个辅助结点,指向链表的最后一个结点
Boy temp = first;
while(temp.getNext() != first){
temp = temp.getNext();
}
//循环添加
for(int i = 2; i < nums + 1;i++){
Boy boy = new Boy(i);
temp.setNext(boy);//插入
boy.setNext(first);//构成环
temp = boy;//temp继续指向结尾
}
}
遍历环形链表
1.先让一个辅助指针cur,指向first结点
2.然后通过一个while循环遍历该链表,cur == first结束
/**
* 遍历链表
*/
public void show(){
Boy temp= first;
while(true){
System.out.println(temp.toString());
if(temp.getNext() == first){
break;
}
temp = temp.getNext();
}
}
3.2约瑟夫问题实现
根据用户的输入,生成一个小孩出圈的顺序
n = 5 , 即有5个人
k = 1, 从第一个人开始报数
m = 2, 数2下
实现
1.需要创建一个辅助指针helper,事先应该指向环形链表的最后这个节点
补充:小孩报数之前,先让first和helper移动k-1次,即开始报数位置
2.当小孩开始报数时,让first和helper同时移动m-1次
3.这是就可以将first指针指向小孩结点 出圈
first = first.next;
helper.next = first;
原来first指向的节点就没有任何引用了,就会被会少
出圈顺序: 2—4---1—5---3
代码实现
/**
* 报数
* @param start 报数起始位置
* @param k 报到k的人出列
* @param nums 总人数
*/
public void numberOff(int start,int k,int nums){
//1.验证k值和start以及nums值是否合法
if(nums <= 0){
System.out.println("nums值不合法!");
return;
}
else if(k <=0 ){
System.out.println("k值不合法!");
return;
}
else if(start <= 0 || start > nums){
System.out.println("start值不合法!");
return;
}
//2.创建环形队列并打印
add(nums);
System.out.println("孩子站队编号如下:");
show();
System.out.println("********************************");
//3.报数
//首先要找到起始位置
//先让temp指向尾结点,然后temp和first前进start-1次
Boy temp = first;
while(temp.getNext() != first){
temp = temp.getNext();
}
//temp前进start-1次
for(int i = 0; i < start-1;i++){
temp = temp.getNext();
first = first.getNext();
}
System.out.println("报数起始位置:"+start);
//循环,每前进k-1次执行一次出列
while(true){
//只有一个结点时,报数出循环
//因为temp在first的后面,所以二者相遇时表示只有一个结点
if(first == temp){
System.out.println("最后出列的编号是:"+temp.getNo());
break;
}
//前进k-1步
for(int i = 0; i < k -1; i++){
first = first.getNext();
temp = temp.getNext();
}
//执行出列
System.out.println("孩子编号为"+temp.getNext().getNo()+"出列");
temp.setNext(first.getNext());
first = temp.getNext();
}
}
temp指向尾结点,然后temp和first前进start-1次
Boy temp = first;
while(temp.getNext() != first){
temp = temp.getNext();
}
//temp前进start-1次
for(int i = 0; i < start-1;i++){
temp = temp.getNext();
first = first.getNext();
}
System.out.println(“报数起始位置:”+start);
//循环,每前进k-1次执行一次出列
while(true){
//只有一个结点时,报数出循环
//因为temp在first的后面,所以二者相遇时表示只有一个结点
if(first == temp){
System.out.println("最后出列的编号是:"+temp.getNo());
break;
}
//前进k-1步
for(int i = 0; i < k -1; i++){
first = first.getNext();
temp = temp.getNext();
}
//执行出列
System.out.println("孩子编号为"+temp.getNext().getNo()+"出列");
temp.setNext(first.getNext());
first = temp.getNext();
}
}