单链表
单链表
概述
百度百科:单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
简单来说,链表就是,把一个一个的数据集给串起来,然后形成的串串。数据集在链表中就称为节点
Java中的链表和C语言的比较类似,不过这里我们不需要使用指针
节点:
链表是由节点组成的,一个节点存储的数据分为两部分,第一部分是我们需要存储的数据,另一部分是下一个节点的地址(即引用)。C语言中节点是结构体,在Java中,创建节点类,用对象替代节点。每个节点必须含有一个属性用来指向下一个节点,此属性类型设置为节点类,此属性作用等同于C语言结构体中的next指针,即存储下一个节点对象的地址。
no是编号,name是姓名
头结点:
单链中有一个很重要的节点,即头头节点,单链表,头节点就是一切,没了头节点整个单链表就无法遍历了。头结点可以不存放数据,仅存储下一个节点的地址,也可用存放数据。
我们这里使用头结点仅存储下一个节点的地址。
增删改+打印
插入节点(创建)
这里,创建头结点,同时创建一个节点对象,把head节点的地址也赋值为新建的节点,防止直接在head上面动刀
这里为了测试的时候记忆操作,把方法的形参设置为一个数组,到时候传入存放节点的数组,再由此对数组遍历,然后插入节点。
修改
首先接入新节点(形参),此处也是仅记录一个简单的修改的思路,找到节点,然后修改
删除
此处仅删除一个固定的节点,仅记录一下删除的思路,就是当某个节点不被别人引用,则等于她就被删除了,然后让被删除的节点的上一个节点指向被删除节点的下一个节点
打印链表
简单
几个面试题
单链表在面试中也是热点话题
求链表中有效节点个数
这个简单,直接遍历即可
查找倒数第K个节点
查找倒数第K个节点,因为单链表的特殊,我们不可能从链表尾遍历,所以需要先得出链表有几个有效节点,然后用有效节点数减去K,即得到从第一个有效节点开始到倒数第K个节点间需要遍历的次数
单链表的反转
所谓单链表的反转,就是把原来的链表反过来,即比如12345变为54321
此处思路是根据原链表来进行头插法,从而得到一个新链表,再把原链表的头指向新链表的第一个有效节点,按照单链表的特性(找到头结点,就等于找到了整个链表),此时便实现了链表的反转。
具体的实现方法:遍历原链表,每遍历一个节点,就把它从这个链表中"取出",即让他的next域指向新链表的第一个有效节点(当新链表只有头结点,则正好指向NULL,表示此节点会作为新链表的链表尾),然后使新链表的头的next域再指向此节点,这样便实现了一个节点的删除和一个节点的插入,此处的插入便是头插法
单链表的反向打印
单链表的反向打印也可以先把链表反转,再打印,再反转回去,但是不仅扯淡,还效率低。
我们可以使用栈,栈也是一种数据容器,类似于集合,他的特点是先进后出
可以使用Stack<T> 名=new Stack<T>();
来创建一个栈
栈的存放数据可以使用他的push()
或add()
方法
取数据可以使用pop()
方法,不能指定取的数据,会自动根据进入的先后顺序来取出
size()
方法可以返回当前栈中的元素数
两链表合并,并保持顺序一致性
思路,可以遍历其中一个链表,每遍历一个节点,便插入到另一个链表中,在插入的过程中同时判断其序号大小来进行排序
因为此链表为了测试方便,把add方法的参数写成了数组,所以此处需要把一个链表中的有效节点全部存入数组,再把数组传给add方法来进行插入
测试
测试部分代码,结果就不截图了,麻烦
public static void main(String[] args) {
Node node1=new Node(1,"松江");
Node node2=new Node(3,"吴用");
Node node3=new Node(2,"卢俊义");
Node node4=new Node(3,"吴用");
Node node5=new Node(4,"牛蛙一号");
Node node6=new Node(5,"牛蛙二号");
Node node7=new Node(6,"牛蛙三号");
Node node8=new Node(7,"牛蛙四号");
Node node9=new Node(8,"牛蛙五号");
Node node10=new Node(10,"牛蛙五号");
Node node[]={node6,node7,node8,node9};
Node node0[]={node1,node2,node3,node4,node5,node10};
SingleLinkedList linkedList=new SingleLinkedList();
SingleLinkedList linkedList1=new SingleLinkedList();
linkedList.add(node);
linkedList1.add(node0);
Scanner sc=new Scanner(System.in);
String n="1";
while(true){
System.out.print("0,退出\n"+"1,修改\n"+"2,显示所有\n"+"3,删除\n"+"4,得到倒数第K个节点\n"+"5,链表反转\n"+"6,链表反向打印\n"+"7,两链表合并\n");
n = sc.next();
if(n.equals("0")){
break;
}else if (n.equals("1")){
linkedList.update(new Node(6,"大头"));
linkedList.list();
}else if(n.equals("2")){
linkedList.list();
}else if(n.equals("3")){
linkedList.delect(3);
linkedList.list();
}else if(n.equals("4")){
int K=0;
String K1=null;
while (true){
System.out.println("请输入K值");
K1=sc.next();
try {
int k= Integer.parseInt(K1);
break;
}catch (Exception e){
System.out.println("输入错误");
continue;
}
}
try {
Node node000=linkedList.getLastNode(K);
System.out.println(node000);
}catch (Exception e){
System.out.println(e.getMessage());
}
}else if(n.equals("5")){
linkedList.reversal();
linkedList.list();
}else if(n.equals("6")){
linkedList.reversalShow();
}else if(n.equals("7")){
linkedList.merge(linkedList1);
linkedList.list();
}else {
System.out.println("输入错误");
}
}
System.out.println("退出成功");
}
所有代码
如下:
import java.util.Scanner;
import java.util.Stack;
public class SingleLinkedListDemo {
public static void main(String[] args) {
Node node1=new Node(1,"松江");
Node node2=new Node(3,"吴用");
Node node3=new Node(2,"卢俊义");
Node node4=new Node(3,"吴用");
Node node5=new Node(4,"牛蛙一号");
Node node6=new Node(5,"牛蛙二号");
Node node7=new Node(6,"牛蛙三号");
Node node8=new Node(7,"牛蛙四号");
Node node9=new Node(8,"牛蛙五号");
Node node10=new Node(10,"牛蛙五号");
Node node[]={node6,node7,node8,node9};
Node node0[]={node1,node2,node3,node4,node5,node10};
SingleLinkedList linkedList=new SingleLinkedList();
SingleLinkedList linkedList1=new SingleLinkedList();
linkedList.add(node);
linkedList1.add(node0);
Scanner sc=new Scanner(System.in);
String n="1";
while(true){
System.out.print("0,退出\n"+"1,修改\n"+"2,显示所有\n"+"3,删除\n"+"4,得到倒数第K个节点\n"+"5,链表反转\n"+"6,链表反向打印\n"+"7,两链表合并\n");
n = sc.next();
if(n.equals("0")){
break;
}else if (n.equals("1")){
linkedList.update(new Node(6,"大头"));
linkedList.list();
}else if(n.equals("2")){
linkedList.list();
}else if(n.equals("3")){
linkedList.delect(3);
linkedList.list();
}else if(n.equals("4")){
int K=0;
String K1=null;
while (true){
System.out.println("请输入K值");
K1=sc.next();
try {
int k= Integer.parseInt(K1);
break;
}catch (Exception e){
System.out.println("输入错误");
continue;
}
}
try {
Node node000=linkedList.getLastNode(K);
System.out.println(node000);
}catch (Exception e){
System.out.println(e.getMessage());
}
}else if(n.equals("5")){
linkedList.reversal();
linkedList.list();
}else if(n.equals("6")){
linkedList.reversalShow();
}else if(n.equals("7")){
linkedList.merge(linkedList1);
linkedList.list();
}else {
System.out.println("输入错误");
}
}
System.out.println("退出成功");
}
}
//管理链表的类
class SingleLinkedList{
// 创建头结点,头结点仅作为一个标记,不存放数据
private Node head=new Node(0,"");
private Node temp; //防止head头结点发生变动,此处创建一个节点用来代替头结点
// 增加节点的方法
public void add(Node[] nodeArray){
temp=head;
// 将遍历链表的操作和插入节点的操作分开
// 设置一个标记,用于判断编号是否重复
boolean flag;
// 遍历,按照编号从小到大,找到新增节点应该插入的位置
for (int i = 0; i < nodeArray.length; i++) {
// 设置初始值为flase,当编号重复,则设置flag变为true
flag=false;
while(true){
if (temp.next==null){
break;
}
if (temp.next.no>nodeArray[i].no){
break;
}
if (temp.next.no==nodeArray[i].no){
flag=true;
break;
}
temp=temp.next;
}
// 插入操作
if (flag){
System.out.println(nodeArray[i].no+"编号重复");
continue;
}else {
nodeArray[i].next=temp.next;
temp.next=nodeArray[i];
}
}
}
// 删除
public void delect(int no){
if (head.next==null){
System.out.println("链表为空");
return;
}
// 遍历 查找
temp=head;
while (true){
if (temp.next==null){
System.out.println("未找到");
return;
}
// 删除需要用temp.next去查找,不能用temp。
// 如果用temp当找到要被删除的数据,则五法找到其前一个节点,然后就没法删除
if (temp.next.no==no){
break;
}
temp=temp.next;
}
// 找到要删除的节点后,使此节点的前一个节点和此节点的后一个节点相连接,那么此节点不被引用,则会被垃圾回收
temp.next=temp.next.next;
}
// 显示链表所有的数据
public void list(){
if (head.next==null){
System.out.println("链表空");
}else {
temp=head.next; //头结点无数据,所以直接让temp指向第二个节点
while (temp!=null){
System.out.println(temp);
temp=temp.next;
}
}
}
// 修改节点
public void update(Node node){
// 首先判断链表是否为空
if (head.next==null){
System.out.println("链表为空");
return;
}
temp=head.next;
// 这里遍历查找对应编号的节点,遍历至使temp指向要修改的节点
while (true){
if (temp==null){
// 遍历到链表尾那就说明未找到,直接return
System.out.println("未找到");
return;
}
if (temp.no==node.no){
break;
}
temp=temp.next;
}
// 找到后对链表数据进行修改
temp.name=node.name;
}
// 得到有效节点个数
public int getLong(){
temp=head;
int n=0;
while(temp.next!=null){
temp=temp.next;
n++;
}
return n;
}
// 得到倒数第K个节点
public Node getLastNode(int n){
int Long=getLong();
if (Long==0){
throw new RuntimeException("链表为空");
}
temp=head.next;
for (int i = 0; i < Long-n; i++) {
temp=temp.next;
}
return temp;
}
// 链表反转
public void reversal(){
if (head.next==null||head.next.next==null){
System.out.println("数据不足,无需反转");
return;
}
Node head1=new Node(0,"");
temp=head.next;
Node next =null;
while (temp!=null) {
next = temp.next;
temp.next=head1.next;
head1.next=temp;
temp=next;
}
head.next=head1.next;
}
// 链表反向打印
public void reversalShow(){
if (head.next==null){
System.out.println("链表空");
return;
}
temp=head.next;
Stack<Node> stack=new Stack<Node>();
while (temp!=null){
stack.push(temp);
temp=temp.next;
}
while (stack.size()>0){
System.out.println(stack.pop());
}
}
// 得到头结点
public Node getHead(){
return head;
}
// 把另一个链表并入此链表
public void merge(SingleLinkedList linkedList1){
int n=linkedList1.getLong();
temp=linkedList1.getHead().next;
if (n==0){
System.out.println("第二联表为空");
return;
}
Node node[]=new Node[n];
for (int i = 0; i < n; i++) {
node[i]=temp;
temp=temp.next;
}
add(node);
}
}
//创建节点类
class Node{
public int no;
public String name;
public Node next=null;
public Node(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "node{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
双向链表
概述
双向链表和单链表都是线性结构,不同的是双向链表他是双向的,除头和尾节点,其他每个节点不仅可以指向下一个节点,也指向此节点的上一个节点。即可以直接根据一个节点找到他的上一个节点。这样的操作让链表的删除操作更加简单
双向链表的实现
节点的设计
比单链表的节点多一个节点类型的pre属性,用于指向节点的上一个节点
节点的插入
按照从小到大插入
删除
删除操作要比单链表的删除操作简单
全部代码
import java.util.Scanner;
public class DoubleLinkedListDemo {
public static void main(String[] args) {
NodeDouble node1=new NodeDouble(1,"松江");
NodeDouble node2=new NodeDouble(3,"吴用");
NodeDouble node3=new NodeDouble(2,"卢俊义");
NodeDouble node4=new NodeDouble(3,"吴用");
NodeDouble node5=new NodeDouble(4,"牛蛙一号");
NodeDouble node6=new NodeDouble(5,"牛蛙二号");
NodeDouble node7=new NodeDouble(6,"牛蛙三号");
NodeDouble node8=new NodeDouble(7,"牛蛙四号");
NodeDouble node9=new NodeDouble(8,"牛蛙五号");
NodeDouble node10=new NodeDouble(10,"牛蛙五号");
NodeDouble node0[]={node1,node2,node3,node4,node5,node10,node6,node7,node8,node9};
DoubleLinkedList linkedList=new DoubleLinkedList();
linkedList.add(node0);
Scanner sc=new Scanner(System.in);
String n;
while(true) {
System.out.print("0,退出\n" + "1,显示所有\n" + "2,删除\n");
n = sc.next();
if (n.equals("0")) {
break;
}else if(n.equals("1")){
linkedList.list();
}else if (n.equals("2")){
int K=0;
String K1;
while (true){
System.out.println("请输入K值");
K1=sc.next();
// 为保证程序的健壮性,这里防止用户输入的不是数字,此处做一个整形转换,用try catch包裹一下,转换失败则让用户重新输入
try {
int k= Integer.parseInt(K1);
linkedList.delect(k);
break;
}catch (Exception e){
System.out.println("输入错误");
continue;
}
}
}
}
System.out.println("退出成功");
}
}
//管理链表的类
class DoubleLinkedList{
private NodeDouble head=new NodeDouble(0,""); //创建头结点,头结点不存放数据
private NodeDouble temp=null; //temp用于保存head
// 插入节点
public void add(NodeDouble node1[]){ //这里设置的行参是数组是为了方便一次出入多个节点
NodeDouble node=null; //先创建一个node类型的变量,用于后面从数组里取数据
temp = head; //初始化temp
boolean flag; //定义一个标记,在后面用于判断节点编号是否重复
for (int i = 0; i <node1.length ; i++) { //这里的for循环不重要,用于遍历得到数组中的数据
flag=false; //每次循环进来把flag默认设置为flash
node=node1[i]; //把数组中的数据放在变量中
while (temp.next != null) { //while循环体用于比较节点的编号,根据链表从小到大的顺序来插入数据
// 此处对链表进行遍历下面是依次比较链表中节点编号的大小,当找到的节点编号比传入节点编号大的时候,则跳出循环
if (node.no < temp.next.no) {
break;
} else if (node.no == temp.next.no) { //当编号重复,则令flag变为true
flag=true;
break;
}
temp = temp.next;
}
if(flag) { //当flag为true,表示编号重复,则跳过这次的for循环
System.out.println(node.no+"编号重复,此节点自动忽略");
continue;
} else if (temp.next == null) { //当temp.next==null,则表示遍历到链表尾,直接节点添加到链表尾
// 下面两行用于将节点连接到链表尾
temp.next = node;
node.pre = temp;
} else {
// 节点插入位置不是尾部则需要四次操作
node.next = temp.next;
temp.next.pre = node;
temp.next = node;
node.pre = temp;
}
}
}
// 显示链表所有的数据,和单链表一模一样
public void list(){
if (head.next==null){
System.out.println("链表空");
}else {
temp=head.next; //头结点无数据,所以直接让temp指向第二个节点
while (temp!=null){
System.out.println(temp);
temp=temp.next;
}
}
}
// 删除。删除操作比单链表节点,不需要刻意的留存待删除节点的上一个节点
public void delect(int no){
if (head.next==null){ //正常的先判断链表是否为空
System.out.println("链表为空");
return;
}
// 遍历 查找
temp=head.next; //直接初始化temp为链表第一个有效节点
while (true){ //while循环用于遍历链表查找要删除的节点
if (temp==null){ //当temp==null,则表示遍历结束了,此时若还未找到,则表示链表中没有对应编号的数据
System.out.println("未找到");
return;
}
if (temp.no==no){ //找到直接退出循环
break;
}
temp=temp.next; //更新temp值
}
// 找到要删除的节点后,使此节点的前一个节点和此节点的后一个节点相连接,那么此节点不被引用,则会被垃圾回收
temp.pre.next=temp.next;
}
}
//创建节点类
class NodeDouble{
public int no;
public String name;
public NodeDouble next=null; //next用于指向节点的下一个节点
public NodeDouble pre=null; //pre用于指向节点的上一个节点
public NodeDouble(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "node{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
注意事项
这里需要多多体会temp用head或head.next的区别差异,同时!精华都在注释中!