认识链表及链表的原理

在开始链表之前,我们先来看一下 ArrayList的问题及思考:

1. ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考: 如何解决以上问题呢?

优点:

1.在给定下表进行查找的时候,时间复杂度为O(1)

总结:顺序表比较适合进行给定下表查找的场景 

这里我们期待的是1.能不能每方一个元素分配一块空间,每方一个元素分配一块空间,

2.能不能插入数据的或者删除数据的时候不移动数据 

所以在这种情况下就有了一种新的数据结构,这种数据结构就是链表,他能解决我们上面说的顺序表所出现的缺点。

1.链表的概念及结构

链表是由节点组成,节点又由两部分组成,一部分是数据域(起名为data或者value),一部分是next域,如下:

 

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。 

 如上每个节点都是独立的,我们通过next域把他们串起来。 

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向或者双向


2. 带头或者不带头 

3. 循环或者非循环

  虽然有这么多的链表的结构,但是我们重点掌握两种:  

  • 单向不带头非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

  • 双向不带头非循环链表:在Java的集合框架库中LinkedList底层实现就是双向不带头非循环链表。

2.链表的实现原理 

public interface IList {
 //头插法
void addFirst(int data);
//尾插法
void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
void addIndex(int index,int data);
//查找是否包含关键字key是否在单链表当中
boolean contains(int key);
//删除第一次出现关键字为key的节点
void remove(int key);
//删除所有值为key的节点
void removeAllKey(int key);
//得到单链表的长度
int size();
//清空
void clear();
//打印
void display();
}

将上面这些接口重写,重写之后有人就想,实现这些和方法的前提是什么?当然是链表,这个链表本身的数据是通过我们不断地插入,不断地插入而获得的。

第一个问题是得有一个一个的节点,这个节点从哪里来呢,这里把这个节点定义成内部类。这里肯定有人问,为什么定义成内部类,因为内部类的定义是如果说MySingleList这个类本身是由若干的完整的一个类而组成的,所以我们把这个类定义成内部类。

而ListNode有两个成员,第一个成员是value域,第二个是next域,假设我们这个链表存整形的情况下首先第一个存value域,其次有一个next域,而next存的是下一个节点的地址(节点是一个对象),就相当于说这里面有一个Person per=new  Person();new  Person()是 person对象(相当于ListNode)。 per是节点对象,它有一个地址会存在引用里面,这个引用类型是 person类型的引用.那么也意味着,ListNode就是类型,如下图的每一个节点的类型都是ListNode,而我定义了这个类型的变量(public ListNode next;),而next是下一个节点的地址,而下一个节点是 ListNode 对象,所以它next的类型一定是 ListNode(这个next类型的引用是节点的类型),所以我们在这个过程中定义一个ListNode next,图2和代码如下

 //把节点定义成内部类
    static  class ListNode{//节点
        //整形情况下
        public int val;
        public ListNode next;
    }

如上节点有了,有两个成员属性。假如现在在如上图中要插入一个66,此时我们在new这个节点的时候并不知道这个66往哪里插,所以我们不知道他的next是多少,所以再给他提供这个构造方法的时候只能提供如下的构造方法,因为我们加一个next也不知道next是谁,没有初始化next他就是null,因为他是引用数据类型,所以这个内部类我们把他叫节点的内部类。

这里把他定义成静态内部类的原因是因为静态内部类对象不需要通过外部类对象的引用而获得。

//把节点定义成内部类
    //节点的内部类
    static  class ListNode{
        //整形情况下
        public int val;
        public ListNode next;
        
        public ListNode(int val){
            this.val=val;
        }
    }

所以说,单链表他有如上的内部类 ,它是由若干的节点组成的。

对于链表本身来说,它是由一个一个节点组成的,不管你带不带头,它总有第一个,对于带头的来说(如1),它是头节点,对于不带头的来说(如2),它第一个也是头节点。

但是它量的区别在于1的头节点永远不会改变。2的头节点有可能发生改变,比如在2的前面再插入一个节点,他的头节点就会改变。

那也就意味着,对于链表本身来说,链表的属性是hand,所以我们需要定义一个ListNode head,他现在指向谁我们也不知道。如下:

 //把节点定义成内部类
    //节点的内部类
    static  class ListNode{
        //整形情况下
        public int val;
        public ListNode next;

        public ListNode(int val){
            this.val=val;
        }
   }
        public ListNode hand;
    

这里我们想一想,为什么不把 ListNode head定义在内部类里面呢?

因为ListNode head他是属于链表(MySingleList)的成员,不是每一个节点的成员,每一个节点的成员只有两个。所以hand应该定义在内部类的外面。

接下来,我们先不说头插尾插等方法,先来看另一种方式(虽然比较low,但是可以帮助理解)。

先写一个方法,如下:

public void creatList(){
            
 }

 在这个方法中假设有 如上图的5个节点,其代码如下:

         public void creatList(){
            //第一个节点
            ListNode node1=new ListNode(12);
            //第二个节点
            ListNode node2=new ListNode(23);
            //第三个节点
            ListNode node3=new ListNode(34);
            //第四个节点
            ListNode node4=new ListNode(45);
            //第五个节点
            ListNode node5=new ListNode(56);
        }

当我们现在new完这些节点之后,就呈现如下图变化。

此时,很多人肯定会问,为什么new的5个节点这个地方为空呢?

因为我们在实例化调用带有一个参数的构造方法的时候没有初始化next(如上图)

也意味着 ,在实例化如上第一幅图中的next的时候,都为空。那现在上面5个节点写完之后他们都是独立的。node1这个引用变量里面存的是0x85,node2这个引用变量里面存的是0x46,node3这个引用变量里面存的是0x81,node4这个引用变量里面存的是0x97,node5这个引用变量里面存的是0x12,这就说明五个节点都有了,5个引用都有了,每个引用都存了这个节点的地址。此时,我们有以下问题:

如何让第一个节点的下一个节点是node2.

很简单,只要让node1.next=node2 即可。这就意味着node2里面的值0x46给到node1就可以了。

此时代码就可以变一变了,如下:

public void creatList(){
            //第一个节点
            ListNode node1=new ListNode(12);
            //第二个节点
            ListNode node2=new ListNode(23);
            //第三个节点
            ListNode node3=new ListNode(34);
            //第四个节点
            ListNode node4=new ListNode(45);
            //第五个节点
            ListNode node5=new ListNode(56);
            
            node1.next=node2;
            node2.next=node3;
            node3.next=node4;
            node4.next=node5;
        }

逻辑图如下: 

这里我们发现,只要拿到node1,后面其他都会得到。但是这里有一个问题,node1是局部变量,当createList()这个方法调用完成之后,node1在栈上被回收了,这样的话就找不到12这个节点了,那该怎么办呢?很简单,让这个方法的返回值变成ListNode,再在如下代码位置retuen node1,或者不return,返回值就是void,前面代码有hand,让this.hand=node1.如下2

1.
public ListNode creatList(){
            //第一个节点
            ListNode node1=new ListNode(12);
            //第二个节点
            ListNode node2=new ListNode(23);
            //第三个节点
            ListNode node3=new ListNode(34);
            //第四个节点
            ListNode node4=new ListNode(45);
            //第五个节点
            ListNode node5=new ListNode(56);

            node1.next=node2;
            node2.next=node3;
            node3.next=node4;
            node4.next=node5;

            return node1;
        }






2.
public void creatList(){
            //第一个节点
            ListNode node1=new ListNode(12);
            //第二个节点
            ListNode node2=new ListNode(23);
            //第三个节点
            ListNode node3=new ListNode(34);
            //第四个节点
            ListNode node4=new ListNode(45);
            //第五个节点
            ListNode node5=new ListNode(56);

            node1.next=node2;
            node2.next=node3;
            node3.next=node4;
            node4.next=node5;

            this.hand=node1;
        }











注意:
 node1.next=node2;代表如何修改当前节点的next值为指定的节点

这样的话,回收归回收,但是hand本身会把这个局部变量的值记录下来,意味着让node1这个引用指向了引用所指的对象(new ListNode(12)),也意味着直接让hand指向了node1所指的位置。所以当如上所有方法走完了,这些局部变量(node1,node2,node3..)都被回收了,也就没有局部变量了,那么我们只需要通过一个hand就能找到链表当中所有的节点。

如上我们写完了这个代码,此时在测试类中用 MySingleList实例化MySingleList,然后通过mySingleList调用createList(),然后在这里随便打印一个什么东西,然后进行调试,我们可以看到MySingleList这个引用所指向的对象里面一定有一个成员hand,根据刚才所说,hand会从12开始,一直到56,我们发现,通过打印hand能把所有的值都能拿到,这里看一下调试

这里想一个问题,怎样通过hand打印拿到所有的值呢?

接着往下看。

现在的问题就是,怎样如上图一样让hand依次往后走,去遍历这个链表呢?

当然,要遍历这个链表,先想以下问题:

1.是怎么从一个节点走到下一个节点的

2.怎么判断所有的节点都遍历完了

来看第一个问题:
 

此时,如上图,hand(栈上)存的是0x85 (堆上),要让hand走向下一个0x46,只需要让hand的值改成0x46就好了,那怎么让hand改成0x46呢?怎么拿到0x46的值呢?

此时我们可以写这样一行代码,head=head.next;因为现在hand是0x85,而0x85这个对象的next是0x46,把0x46给到这个hand,就意味着hand值就不是0x85了,是0x46了。逻辑图如下:

如上,我们通过不断head=head.next;从第一个节点走到了最后一个节点 ,此时,最后一个节点的next为空了。那我们来看第二个问题:怎么判断所有节点都遍历完了?也就是当hand=null,也意味着hand不指向任何对象了,这个链表就遍历完了。换句话说,当hand不为空,直接通过sout去打印hand的val,打印完成之后,让hand往后走,此时一直往下走必然是一个循环,循环条件是让hand为空,循环结束。如上所说代码如下:

   @Override
    public void display() {
   //hand为空截至,不为空继续打印
        while(hand !=null){
            System.out.print(hand.val+" ");
            hand=hand.next;
        }
    }

打印结果如下:

但是此时有一个问题,如上这样写有什么不好的地方吗?

这里的问题就是当我把display这个方法走完之后,hand就等于null了,0x85没跑,标记0x85的hand跑了 。所以当打印完之后,就找不到hand标记的0x85了。这肯定不行,这里我们让他hand不要动,而是让临时创建的cur走,最后cur为null了,但是hand本身来说没有变。代码如下:

@Override
    public void display() {
        ListNode cur=this.hand;
        while(cur !=null){
            System.out.print(cur.val+" ");
            cur=cur.next;
        }
      System.out.println()
    }



注意:
cur !=null 这段代码的意思是来判断是否遍历完了链表
 cur=cur.next; 代表cur如何从当前位置走到下一个节点的位置

接下来看size

//得到单链表的长度
    int size();

    @Override
    public int size() {
        
            return 0;
    }

之前我们在顺序表当中定义了一个usedSize,当然在这里也能定义,如果不定义 ,就需要一个节点一个节点去计算,那也就意味着,在size方法中假设定义 一个int count=0,我们怎么去求当前链表的节点个数呢?

还是以上图为例,如何求链表的节点个数,首先思路很简单,就是遍历每一个节点 。

这里肯定不能 用hand遍历,而是用cur,cur从0x85这个地方走,cur肯定不为空,不为空就要进行加加,一直往下走,走到cur为空,计数结束。代码如下:

@Override
    public int size() {
            int count=0;
        ListNode cur=this.hand;
        while(cur !=null) {
            cur = cur.next;
            count++;
        }
        System.out.println();
            return count;
    }

 代码结果如下:

接下来看contains

//查找是否包含关键字key是否在单链表当中
    boolean contains(int key);


public boolean contains(int key) {
            return false;
    }

还是以下图为例

这里我们判断一下,看这个链表是否包含34。

这里还是先遍历链表, 如果等于就说明包含返回turn,如果不等于就继续往下一个节点走,如果不包含就返回flase.

 @Override
    public boolean contains(int key) {
            ListNode cur=this.hand;
            while(cur!=null){
                if(cur.val==key){
                    return true;
                }
                //如果不等于往下一个节点走
                cur=cur.next;
            }
            return false;
    }

结果如下:

接下来看addFirst

//头插法
 void addFirst(int data);



@Override
public void addFirst(int data) {

}

以下图为例:

头插法指的是当我们现在要插入一个数据,把这个数据插入到当前链表的第一个位置如图1到图3的过程.

假如这里我们要如上图一样插入66,而我们首先要new一下这个节点,先得有这个节点,有了之后假设他的地址是0x65,他的next为null.我们把这个节点叫做newH.

现在要把它插到如图3的第一个位置,此时,头节点hand就不再是12了,就变成了66 。此时,我们就要想怎么把0x85插入进去呢?然后让他从如图3中66位置开始遍历呢?所以如上我们可以看出,在上图中进行头插的时候改变了两个地方,一个是插入节点的next,一个是让hand走到指向0x65这个地方,这里一共需要三步:

1.实例化一个节点

2.改变插入节点的next

3.改变hand

还有一种情况是这个链表里面一个节点都没有,hand为空不指向任何对象,此时插了一个假设和上面一样叫newH的节点,此时就让hand直接指向newH,他就是第一个节点。所以这里分为这个链表一个节点都没有和已经存在节点两种情况。

代码如下: 

@Override
    public void addFirst(int data) {
        //实例化一个节点对象
        ListNode newH=new ListNode(data);
        //第一种情况,如果hand为空,链表一个节点也没有,则即将插入的节点就变成当前链表的第一个节点
        if(hand==null){
            this.hand=newH;
        }else{
            //第二种情况
            newH.next=this.hand;
            this.hand=newH;
        }
    }

此时打印的时候我们就不用 mySingleList.createList(); 这里我们一个一个进行插入,如下:

此时我们从打印结果可以看出我们插的顺序是12, 23,34,45,56,但是打印结果完全相反了。因为头插法相当于倒序插入,先有12,来了23,插在12的前面,后面一次类推,逻辑如下图:

其实,这个代码不用写第一种情况 ,如下:

    @Override
    public void addFirst(int data) {
        //实例化一个节点对象
        ListNode newH=new ListNode(data);
            //第二种情况
            newH.next=this.hand;
            this.hand=newH;
        }
    

接下来看尾插法addLast。

 //尾插法
    void addLast(int data);



    @Override
    public void addLast(int data) {

    }

(尾插法指的是将待插入的节点存放在链表的最后一个位置 )

以下图为例,假如有一个节点node(这里应该是newH,这里写错了,上面图也写成node了,但是原理一样),尾插法就是将这个节点 插入到下图链表的最后位置,如下1到2图:

( 如果如上图2一样有很多个节点,将0x65插进去之后,要将0x65这个节点的next置为空,将其的前一个节点的0x12这个节点的next等于 0x65即可)

这里 一共需要三步:

1.实例化一个节点

2.找到最后一个节点cur

3.cur.next=node;

这里我们就要想,什么时候就是最后一个节点,我们上面头插法说了,cur!=null的时候才把链表走完了,但是最后的结果是cur为空。但是现在跟上面说的不一样,这里我们让cur指向最后一个节点的位置。也意味着让cur走到0x12,而0x12的next是null,然后把node插入进去。

再换一个角度,如果这个hand为空,那插入的第一个节点也是尾插,让hand指向node就可以了,如图3. 

代码如下:

@Override
    public void addLast(int data) {
        //实例化一个节点对象
        ListNode newH=new ListNode(data);
        ListNode cur=this.hand;
        //第一种情况
        if(this.hand==null){
            this.hand=newH;
        }else{
            //第二种情况
            //找到尾巴
            while(cur.next!=null){
                cur=cur.next;
            }
            //cur 现在指向了最后一个节点
            cur.next=newH;
        }
    }






cur.next!=null 当我们看到这段代码的时候,说明cur指向了最后一个节点

代码结果:

注意看一下代码结果顺序,和上面头插法的不同。 

分析:

头插法的时间复杂度O(1)

尾插法的时间复杂度O(N)

因为尾插法要找尾巴而头插法不用

但是我们发现不管怎么样他们的插入是不需要去移动元素的,而是要一个new一个,要一个new一个,所以链表的优点就是插入,只需要改变指向就可以了。

结论:

根据头插法和尾插法代码,我们得出这样的结论:

1.如果想让cur停在最后一个节点的位置则 cur.next!=null

2.如果想把整个链表的每个节点都遍历完那么就是 cur!=null

接下来看addINdex

//任意位置插入,第一个数据节点为0号下标
 void addIndex(int index,int data);


@Override
 public void addIndex(int index, int data) {

  }

(任意位置插入一个节点) 

还是以下图为例, 假如现在要把0x65插入到0x81这个位置,那相当于把0x81挤走了,但并不是移动元素,而是如下图1这样,最终结果如图2,此时就将0x65插入进去了。

此时的问题是cur应该往那走呢?看图1,比如cur要往0x81位置走,cur走两步到0x81可不可以这样走呢?当然不行,因为我们这个地方是单链表,你走到0x81这个位置相对于我们这个方法来说没用,因为我们不知道0x81的前一个什么,必须要把0x81的前一个的next改成node.假如走到0x81位置了,就走不回去了,因为他是单链表。相反如果让cur走到0x46这个节点,就很简单了,他的next等于0x65,node.next是0x81,就可以了,所以这里找0x46这个节点是最关键的。那我们应该怎么找这个节点呢?

如果我们能找到cur所在的位置,而此时我们要往进插的0x65的next是null,此时只需要将0x65的next变成0x81即可,而0x81是0x46的next.也就意味着 node.next=cur.next;这个代码说明了0x65的next变成0x81,接下来我们只需要让 cur.next=node;即可。

整理一下上面所说的思路:

1.让cur走index-1步(假如cur是0x81,cur往前走一步走到0x46)

2.实现上面说的两步即可

node.next=cur.next;(这个cur.next是0x81)

cur.next=node;(这个cur.next是0x46)

注意:这两行代码不能互换,这里一定要记住先绑后面,在拉前面。

结论:

在插入一个结点的时候,一定要先绑定后面这个节点,然后再做其他的

 这里注意一下,index不能为负数。还有index可以往0x12后面插,不能往0x12后面的后面插,所以我们要检查一下index的合法性,这里我们自定义一个异常。如果index等于0,进行头插法,如果index等于size(),进行尾插法,否则,index就属于在中间任意位置插,首先我们先要找到0x81的前一个,也就是cur所在的位置。

这里我们就要写一个返回前一个节点的位置的方法searchPrev,这个方法主要是找到index地址的前一个地址。这里我们定义一个cur,从头开始走.要找到0x81这个位置,只需要让他走index-1步。

这个方法写成之后,我们再回到前面的代码中。此时我们通过searchPrev(index)先找到cur所在的位置。这个时候先得有一个节点,这里new一个节点node,然后让node的next等于0x81(0x81是当前的cur的next),接下来就让cur.next=node,这样就在这个地方直接插入进来了。代码如下:

 @Override
    public void addIndex(int index, int data) {
        //检查是否合法
        if(index<0 || index>size()){
            //抛一个自定义的异常
            return;
        }
        //进行头插法
        if(index==0){
            addFirst(data);
            return;
        }
        //进行尾插法
        if(index==size()){
            addLast(data);
            return;
        }
        //先找到cur所在的位置
        ListNode cur=searchPrev(index);
        ListNode node=new ListNode(data);
        node.next=cur.next;
        cur.next=node;
    }
    //找到index的前一个地址
    private ListNode searchPrev(int index){
            //定义一个cur,让从头开始走
            ListNode cur=this.hand;
            //第一种写法
//            while(index-1!=0){
//                cur=cur.next;
//                index--;
//            }
        //另一种写法
        int count=0;
        while(count!=index-1){
            cur=cur.next;
            count++;
        }
        return cur;
    }

代码结果如下:

如上代码没有自定义异常,以下是有自定义异常的代码:

public class PosIllegelity extends RuntimeException{
    public PosIllegelity(String msg){
        super(msg);
    }
}
//检查是否合法
    private void checkIndex(int index){
        if(index<0 || index>size()){
           System.out.println("不合法!");
           throw new PosIllegelity("插入节点异常");
        }
    }

    @Override
    public void addIndex(int index, int data) {
        //检查是否合法
//        if(index<0 || index>size()){
//            //抛一个自定义的异常
//            return;
//        }
        try {
            checkIndex(index);
        }catch (PosIllegelity e){
            e.printStackTrace();
        }

        //进行头插法
        if(index==0){
            addFirst(data);
            return;
        }
        //进行尾插法
        if(index==size()){
            addLast(data);
            return;
        }
        //先找到cur所在的位置
        ListNode cur=searchPrev(index);
        ListNode node=new ListNode(data);
        node.next=cur.next;
        cur.next=node;
    }
    //找到index的前一个地址
    private ListNode searchPrev(int index){
            //定义一个cur,让从头开始走
            ListNode cur=this.hand;
            //第一种写法
//            while(index-1!=0){
//                cur=cur.next;
//                index--;
//            }
        //另一种写法
        int count=0;
        while(count!=index-1){
            cur=cur.next;
            count++;
        }
        return cur;
    }

代码结果:

接下来看remove

//删除第一次出现关键字为key的节点
    void remove(int key);



@Override
public void remove(int key) {
         
 }

还是以下图为例,假如我们现在要删除图中的34,最终我们想要的效果就是如图2这样。

但是这里并没有我们想象的那么容易,比如,我们删的34不一定在链表里面 ,所以,这就相当于我们定义了一个cur,让他遍历链表,假如cur找到了34,我们要删除,怎么删?假设此时cur已经走到了如上图 1中 0x81这个位置,但是我们根本不知道他的前一个是谁。所以这里不能直接走到34这个地方。

假设cur此时在第一个节点0x85这里,这里我们去判断,当前这个节点next.val是不是34,如果不是,继续向下走,到0x46这个节点,这个节点的next.val是34,它就是我们要删除的34的前面,如上图1,此时代码为cur.next=del.next,而del=cur.next.

所以,我们只需要判断cur.next.val==key,那么此时我们就找到了我们要删除的关键字key的前一个。因为这个要一直往下找,所以他是循环语句,他的循环条件是cur.next不等于空(这里的循环条件不能是cur不等于空,因为这样就重复走了)。

整体思路如下:

1.先找到前驱

2.判断返回值是否为空(不为空打的时候才能删除)

3.删除

注意:如果这里要删除头节点是删不了的,因为这里根本就没用去解决第一个头结点的问题。

就是说我们cur在头节点0x85这个地方往后走的时候,根本没有顾及它。假如这里我们要删除头节点,只需要让hand走到0x46这个地方即可。 

 代码如下:

 //找到关键字key的前一个节点的地址
    private ListNode findPrev(int key){
        ListNode cur=this.hand;//从头开始
        while(cur.next!=null){
            if(cur.next.val==key){
                return cur;
            }
            cur=cur.next;
        }
        //如果找不到就返回空
        return null;
    }


    @Override
    public void remove(int key) {
            //一个节点都没有,无法删除
        if(this.hand==null){
            return;
        }
            //删除第一个节点
        if(this.hand.val==key){
            this.hand=this.hand.next;
            return;
        }

        //不是头节点也,节点也不为空的情况
        //找到key前驱
        ListNode cur=findPrev(key);
        //判断返回值是否为空
        if(cur==null){
            System.out.println("没有你要删除的数字!");
            return;
        }
        //删除
        //定义一个del
        ListNode del=cur.next;
        //这里已经删除了34,cur.next指向图中0x97的位置
        cur.next=del.next;

        System.out.println();
    }

结果如下:

接下来看removeAllKey

//删除所有值为key的节点
void removeAllKey(int key);



 @Override
 public void removeAllKey(int key) {

 }

还是以下图为例,现在在这个链表里面,比如说要删23 ,但是这里面有不止一个23,这里我们并不是吧上面的方法在复制一份到这个代码中,而是有要求。

要求:

能否在0(N)的情况下删除(换句话说能不能只遍历一次链表就把所有的23都删除) 

上面我们说过,要删除一个节点,一定要知道它的前驱这里我们定义一个cur在0x46这个位置,定义一个prev在0x85这个位置 ,如上第一幅图。

我们假设定义cur为当前节点,定义prev为当前节点的前驱。这里我们只需要判断一个条件,如果cur.val等于要删除的关键字key,则prev.next=cur.next.这样就把他删除了,删完之后,后面可能还有23,如上第二幅图,此时,我们只需要让cur继续往后走(cur=cur.next),而prev不动。如果cur.next不等于23,让cur再往下一个节点走(此时cur是34,不是23),此时prev也往下走(走到刚才cur走过的位置34,也就是prev=cur),最后就看头节点是不是了即可。代码如下:

 @Override
    public void removeAllKey(int key) {
            //先判断头节点是否为空
        if(this.hand==null){
            return;
        }
        ListNode  prev=hand;
        ListNode cur=hand.next;
        //遍历链表
        while(cur!=null){
            //判断cur是否为空
            if(cur.val==key){
                prev.next=cur.next;
                cur=cur.next;
            }else {
                prev=cur;
                cur=cur.next;
            }
        }
        //判断头节点是不是23
        if(hand.val==key){
            //如果是,只删除头节点
            hand=hand.next;
        }
   
    }

结果如下:

接下来看clear

 //清空
 void clear();




@Override
public void clear() {

}

我们来看一下,如何将下图链表的所有内容清空呢? (注意这里要分基本数据类型和引用数据类型)

这里有一种最粗暴的方式是直接把hand置空(hand=null).还有就是一个一个置空,就是把每个节点的val要置空,next也要置空。

这里我们定义一个cur,假设为引用类型,但是这里不能直接让cur.val=null,cur.next=null,因为当把hand置为空之后,cur就找不到0x46了,所以我们在这里定义一个变量,记录下来cur.next的位置,然后让cur再等于curNext。最后一定要记住将hand也置空。

代码如下:

@Override
    public void clear() {
        ListNode cur=hand;
        while(cur!=null){
            ListNode curNext=cur.next;
            //如果是引用类型,cur.val=null
            cur.next=null;
            cur=curNext;
        }
        //将hand也置空
        hand=null;
    }

    @Override
    public void display() {
        ListNode cur=this.hand;
        while(cur !=null){
            System.out.print(cur.val+" ");
            cur=cur.next;
        }
    }

完整代码如下:

public class MySingleList implements IList {
    //把节点定义成内部类
    //节点的内部类
    static  class ListNode {
        //整形情况下
        public int val;
        public ListNode next;

        public ListNode(int val) {

            this.val = val;
        }
    }
        public ListNode hand;

        public void createList(){
            //第一个节点
            ListNode node1=new ListNode(12);
            //第二个节点
            ListNode node2=new ListNode(23);
            //第三个节点
            ListNode node3=new ListNode(34);
            //第四个节点
            ListNode node4=new ListNode(45);
            //第五个节点
            ListNode node5=new ListNode(56);

            node1.next=node2;
            node2.next=node3;
            node3.next=node4;
            node4.next=node5;

            this.hand=node1;
        }

    @Override
    public void addFirst(int data) {
        //实例化一个节点对象
        ListNode newH=new ListNode(data);
       //第一种情况,如果hand为空,链表一个节点也没有,则即将插入的节点就变成当前链表的第一个节点
       if(this.hand==null){
            this.hand=newH;
        }else{
            //第二种情况
            newH.next=this.hand;
            this.hand=newH;
        }
    }

    @Override
    public void addLast(int data) {
        //实例化一个节点对象
        ListNode newH=new ListNode(data);
        ListNode cur=this.hand;
        //第一种情况
        if(this.hand==null){
            this.hand=newH;
        }else{
            //第二种情况
            //找到尾巴
            while(cur.next!=null){
                cur=cur.next;
            }
            //cur 现在指向了最后一个节点
            cur.next=newH;
        }
    }
    //检查是否合法
    private void checkIndex(int index){
        if(index<0 || index>size()){
           System.out.println("不合法!");
           throw new PosIllegelity("插入节点异常");
        }
    }

    @Override
    public void addIndex(int index, int data) {
        //检查是否合法
//        if(index<0 || index>size()){
//            //抛一个自定义的异常
//            return;
//        }
        try {
            checkIndex(index);
        }catch (PosIllegelity e){
            e.printStackTrace();
        }

        //进行头插法
        if(index==0){
            addFirst(data);
            return;
        }
        //进行尾插法
        if(index==size()){
            addLast(data);
            return;
        }
        //先找到cur所在的位置
        ListNode cur=searchPrev(index);
        ListNode node=new ListNode(data);
        node.next=cur.next;
        cur.next=node;
    }
    //找到index的前一个地址
    private ListNode searchPrev(int index){
            //定义一个cur,让从头开始走
            ListNode cur=this.hand;
            //第一种写法
//            while(index-1!=0){
//                cur=cur.next;
//                index--;
//            }
        //另一种写法
        int count=0;
        while(count!=index-1){
            cur=cur.next;
            count++;
        }
        return cur;
    }

    @Override
    public boolean contains(int key) {
            ListNode cur=this.hand;
            while(cur!=null){
                if(cur.val==key){
                    return true;
                }
                //如果不等于往下一个节点走
                cur=cur.next;
            }
            return false;
    }

    //找到关键字key的前一个节点的地址
    private ListNode findPrev(int key){
        ListNode cur=this.hand;//从头开始
        while(cur.next!=null){
            if(cur.next.val==key){
                return cur;
            }
            cur=cur.next;
        }
        //如果找不到就返回空
        return null;
    }
    @Override
    public void remove(int key) {
            //一个节点都没有,无法删除
        if(this.hand==null){
            return;
        }
            //删除第一个节点
        if(this.hand.val==key){
            this.hand=this.hand.next;
            return;
        }

        //不是头节点也,节点也不为空的情况
        //找到key前驱
        ListNode cur=findPrev(key);
        //判断返回值是否为空
        if(cur==null){
            System.out.println("没有你要删除的数字!");
            return;
        }
        //删除
        //定义一个del
        ListNode del=cur.next;
        //这里已经删除了34,cur.next指向图中0x97的位置
        cur.next=del.next;
        System.out.println();
    }

 @Override
    public void removeAllKey(int key) {
            //先判断头节点是否为空
        if(this.hand==null){
            return;
        }
        ListNode  prev=hand;
        ListNode cur=hand.next;
        //遍历链表
        while(cur!=null){
            //判断cur是否为空
            if(cur.val==key){
                prev.next=cur.next;
                cur=cur.next;
            }else {
                prev=cur;
                cur=cur.next;
            }
        }
        //判断头节点是不是23
        if(hand.val==key){
            //如果是,只删除头节点
            hand=hand.next;
        }
        System.out.println();
    }
    @Override
    public int size() {
            int count=0;
        ListNode cur=this.hand;
        while(cur !=null) {
            cur = cur.next;
            count++;
        }
        System.out.println();
            return count;
    }

    @Override
    public void clear() {
        ListNode cur=hand;
        while(cur!=null){
            ListNode curNext=cur.next;
            //如果是引用类型,cur.val=null
            cur.next=null;
            cur=curNext;
        }
        //将hand也置空
        hand=null;
        System.out.println("=================");
    }

    @Override
    public void display() {
        ListNode cur=this.hand;
        while(cur !=null){
            System.out.print(cur.val+" ");
            cur=cur.next;
        }
       System.out.println();
    }
}

 以上就是链表的增删查改等的实现。 

 

 

         

 

 

 

 

 

 
 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值