下面我们来介绍一种新的数据结构,链表。
我们曾经讨论过顺序表。它的数据存储在物理和逻辑上都是有逻辑的。而我们今天要学习的链表,则在物理结构上非连续存储,逻辑上连续。
1.链表的认识
链表由一个一个的节点组成。
我们可以想象一列火车,每一节车厢都被前面一节拉着,也拉着后面一节(头尾除外)。
我们的链表与火车近似,我们可以将每一个节点当作一节车厢,它除了存储自己的数据之外,还能带领我们找到链接在它后面的一个节点,这样“一节一节”把所有数据串联起来。
单个节点是这样的结构:
地址指的是下一个节点的位置,这样,我们就可以像火车一样把他们串联起来:
(水平有限,意思到了即可)
链表也分很多种类,它可以是双向/单向,不带头/带头,非循环/循环。我们先着重讨论,单项不带头非循环链表。
2.单项不带头非循环链表的实现
同样我们也要先明白链表的实现,我们来先创建一个自己的SingleLinkedList类。
然后,我们来讨论对于节点的定义,我们需要再定义一个节点Listnode类,需要注意,它要被定义在链表这个类里面,它属于一个内部类。它里面要存放值和下一个节点地址,我们可以这样操作:
public class SingleLinkedList {
static class Listnode{
public int val;//节点值域
public Listnode next;//下一个节点的地址
public Listnode(int val) {
this.val = val;
}
}
public Listnode head;//表示当前链表头节点
}
这样我们就完成了对节点的定义,下面我们就要实现链表的各种操作。包括但不限于:创建链表、遍历链表、插入数据(头插/尾插)、删除所有值为k的节点、清空链表等等。同样,我们先把每个方法大的框架定义出来,再一个个实现。
public class SingleLinkedList {
static class Listnode{
public int val;//节点值域
public Listnode next;//下一个节点的地址
public Listnode(int val) {
this.val = val;
}
}
public Listnode head;//表示当前链表头节点
//头插法
public void addFirst(int data){
}
//尾插法
public void addLast(int data){
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
}
//删除所有值为key的节点
public void removeAllKey(int key){
}
//得到单链表的长度
public int size(){
return -1;
}
//清空链表
public void clear() {
}
//遍历打印
public void display() {}
}
creatlist():
首先我们可以手动创建链表,一个节点一个节点进行定义。它并不属于链表的方法,但可以帮助我们理解。我们先创建几个节点,给他们赋值,再用.next一个一个链接,具体可以这样实现:
public void creatlist(){
Listnode listnode1=new Listnode(1);
Listnode listnode2=new Listnode(2);
Listnode listnode3=new Listnode(3);
Listnode listnode4=new Listnode(4);
Listnode listnode5=new Listnode(5);
listnode1.next=listnode2;
listnode2.next=listnode3;
listnode3.next=listnode4;
listnode4.next=listnode5;
this.head=listnode1;
}
不要忘了设置头节点,那么相当于我们已经手动创建出来了一个链表。它大概是这样的:
那么我们的链表就是长这个样子,我们后续操作会基于这种结构进行讲解。
dispaly():
遍历链表,我们要做的就是打印当前节点的值,之后成功走到下一个节点,直到最后,我们可以定义一个新的节点对象,让它指向头节点,从头节点开始一步一步往后走,这样既不会改变头节点的指向,也不会改变链表本身的结构。
public void display() {
Listnode cur=head;
while(cur!=null){
System.out.println(cur.val);
cur=cur.next;//cur往后走
}
}
我们可以测试一下
public static void main(String[] args) {
SingleLinkedList list=new SingleLinkedList();
list.creatlist();
list.display();
}
这就证明向后一步一步走这个策略是没问题的,我们后续很多操作都要遍历链表,我们要始终贯彻这一思想。
插入数据--头插:
头插就是让插入的节点在最开头。我们要贯彻一件事,不管怎么插,头节点永远是第一个节点。那么我们的思想是先将值赋给一个新的节点,让这个节点指向原来的头节点,再改变头节点的指向,这样就可以将新节点与原来的链表连接起来,并且保证了头节点的正确性。
public void addFirst(int data){
Listnode node=new Listnode(data);
node.next=head;
head=node;
}
插入数据--尾插:
这个就会简单一些,我们只需要定义一个cur,让它从头开始往后走,当走到最后一个时(此时是cur.net==null!),把它和新节点连接起来就可以了。
//尾插法
public void addLast(int data){
Listnode node=new Listnode(data);
Listnode cur=head;
while(cur.next!=null){
cur=cur.next;
}
cur.next=node;
}
插入数据--任意位置插入
首先我们要保证pos位置的合法性(头节点位置为0),然后当我们插入时,我们要记录前后两个节点的信息,我们要同时保证前面的可以连接上新节点,并且保证新节点可以连接上后面的,不改变链表的连贯性。所以,我们要找到插入节点的前一个节点的信息,才能方便操作。顺便,当index==0/index==size(),我们可以进行头插尾插,所以我们可以把获取链表长度这个方法写出来。
public void addIndex(int index,int data){
Listnode node=new Listnode(data);
Listnode cur=findIndexSubOne(index);
if(index==0){
addFirst(data);
return;
}
if(index==size()){
addLast(data);
return;
}
cur.next=node.next;
node.next=cur;
}
private Listnode findIndexSubOne(int index){
//找到想要删除/插入节点位置的前一个节点
Listnode cur=head;
while(index-1!=0){
cur=cur.next;
}
return cur;
}
public int size(){
Listnode cur=head;
int cnt=0;
while(cur!=null){
cnt++;
cur=cur.next;
}
return cnt;
}
我们重点体会这样两行代码:
cur.next=node.next; node.next=cur;
这两行是插入的关键。第一行是让新插入节点的后继是后面的节点,第二行则是改变前面节点的指向,指向新插入的节点,这样就既与前面链接,也与后面连上了,且没有改变结构,我们也就实现了中间节点的插入。
contains():
查找是否包含关键字key是否在单链表当中
这个就很简单了,我们遍历一下一带而过就可以了。
public boolean contains(int key){
Listnode cur=head;
while(cur!=null){
if(cur.val==key)
return true;
cur=cur.next;
}
return false;
}
remove():
删除分为两种,一种是删除第一次出现的关键字,一种是删除链表里所有的关键字的节点。它们的思想都是一样的。我们删除的思想是:找到要删除的关键字,把它跳过,就可以了。下面我们来实现一下:
public void remove(int key){
Listnode cur=head.next;
Listnode pre=head;//要删除节点的前驱
while(cur!=null){
if(cur.val==key){
pre.next=cur.next;
return;
}else{
pre=cur;
cur=cur.next;
}
}
//删除头节点
if(head.val==key){
head=head.next;
}
}
public void remove(int key){
Listnode cur=head.next;
Listnode pre=head;//要删除节点的前驱
while(cur!=null){
if(cur.val==key){
pre.next=cur.next;
}else{
pre=cur;
cur=cur.next;
}
}
//删除头节点
if(head.val==key){
head=head.next;
}
}
我们定义一个cur,一个prev,代表当前节点和它的前驱,如果cur走到了要删除的节点,就让prev.next=cur.next,进行跳过。如果没有,就让两“人”都往前走一步,直到最后。当然,这里没有考虑头节点被删除的情况,我们单独实现了一下。
clear():
最后是清空链表,很简单,直接让头节点为空就可以了,这样链表就不存在了。
public void clear() {
this.head=null;
}
那么我们就完成了,下面是完整的代码实现:
public class SingleLinkedList {
static class Listnode{
public int val;//节点值域
public Listnode next;//下一个节点的地址
public Listnode(int val) {
this.val = val;
}
}
public Listnode head;//表示当前链表头节点
public void creatlist(){
Listnode listnode1=new Listnode(1);
Listnode listnode2=new Listnode(2);
Listnode listnode3=new Listnode(3);
Listnode listnode4=new Listnode(4);
Listnode listnode5=new Listnode(5);
listnode1.next=listnode2;
listnode2.next=listnode3;
listnode3.next=listnode4;
listnode4.next=listnode5;
this.head=listnode1;
}
//头插法
public void addFirst(int data){
Listnode node=new Listnode(data);
node.next=head;
head=node;
}
//尾插法
public void addLast(int data){
Listnode node=new Listnode(data);
Listnode cur=head;
while(cur.next!=null){
cur=cur.next;
}
cur.next=node;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
Listnode node=new Listnode(data);
Listnode cur=findIndexSubOne(index);
if(index==0){
addFirst(data);
return;
}
if(index==size()){
addLast(data);
return;
}
cur.next=node.next;
node.next=cur;
}
private Listnode findIndexSubOne(int index){
//找到想要删除/插入节点位置的前一个节点
Listnode cur=head;
while(index-1!=0){
cur=cur.next;
}
return cur;
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
Listnode cur=head;
while(cur!=null){
if(cur.val==key)
return true;
cur=cur.next;
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
Listnode cur=head.next;
Listnode pre=head;//要删除节点的前驱
while(cur!=null){
if(cur.val==key){
pre.next=cur.next;
return;
}else{
pre=cur;
cur=cur.next;
}
}
//删除头节点
if(head.val==key){
head=head.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
Listnode cur=head.next;
Listnode pre=head;//要删除节点的前驱
while(cur!=null){
if(cur.val==key){
pre.next=cur.next;
}else{
pre=cur;
cur=cur.next;
}
}
//删除头节点
if(head.val==key){
head=head.next;
}
}
//得到单链表的长度
public int size(){
Listnode cur=head;
int cnt=0;
while(cur!=null){
cnt++;
cur=cur.next;
}
return cnt;
}
//清空链表
public void clear() {
this.head=null;
}
//遍历打印
public void display() {
Listnode cur=head;
while(cur!=null){
System.out.println(cur.val);
cur=cur.next;//cur往后走
}
}
}
和顺序表一样,我们使用链表也不需要自己重新写,java已经帮我们写好了,我们只需要直接使用就可以了,下面我们来学习链表的使用。
3.单链表的使用
public static void main(String[] args) {
List<Integer>list=new LinkedList<>();
list.add(1);//头插
list.add(2);
list.add(3);
for( int x:list){
System.out.println(x);
}
}
我们可以像这样对单链表对象进行构造,并使用其中的方法,方法大部分都与我们实现的没有太大区别。
4.小结
这篇文章我们主要讨论的是单链表的实现,其中定义cur节点进行遍历这个思想十分重要。单链表十分重要,我们下篇文章会找一些常见典型的单链表的题,通过对题目的的分析加深大家对单链表的印象。