CS61B 课堂笔记

260 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

数据结构之链表

📌声明和赋值:普通类型和引用类型

普通类型

int x;
double y;

在执行上述代码后,我们会得到分别为32位和64位的框,如下图所示: Untitled.png 而当我们为该内存盒赋值的时候,例如,如果我们执行以下代码:

x = -1431195969;
y = 567213.112;

上面的内存狂会被填充如下:

Untitled 1.png

引用类型

我们声明一个Walrus类

public static class Walrus {
    public int weight;
    public double tuskSize;

    public Walrus(int w, double ts) {
          weight = w;
          tuskSize = ts;
    }
}

假如执行下面代码:

Walrus someWalrus;
someWalrus = new Walrus(1000, 8.3);

第一行是引用类型的声明创建一个64位的内存盒,用于存储该引用类型所指向的对象(无论是什么类,引用类型的变量都是64位)。

第二行是引用类型的赋值。new Walrus(1000, 8.3); 会创建一个海象,然后我们会得到一个由32位和64位的盒子共同组成的海象。 Untitled 2.png 等创建好这个海象后,其地址会由new操作符返回,然后将这些位赋值到someWalrus的内存盒中。

Untitled 3.png

引用类型的直接赋值

Walrus a = new Walrus(1000, 8.3);
Walrus b;
b = a;

执行了第一行之后,a中就保存了Walrus的地址。而b = a; 是将a中的地址赋值给b,这样b和a就一起指向了Walrus。

Untitled 4.png 所以任何对b的修改也会影响到a。

🔵IntList

1.定义IntList类

public class IntList {
    public int first;   //数据域
    public IntList rest;    //剩余结点   

    public IntList(int f, IntList r) {
        first = f;   
        rest = r;
    }
}

2.构建5→10→15的链表

如果我们想要构建一个5→10→15的链表,可以这样👇🏻

方法一:从头到尾构建(缺点:如果链表越长,我们需要写的rest越多)

IntList L = new IntList(5, null);
L.rest = new IntList(10, null);
L.rest.rest = new IntList(15, null);

方法二:从尾到头构建(缺点:类似于递归,不太好理解)

IntList L = new IntList(15,null);
L = new IntList (10, L);
L = new IntList (5, L);

3.计算IntList的size的方法

非递归版(链表调用的时候可以返回size)⇒💡 巧用this指针

public int size() {
        int ret = 0;
        IntList p = this;
        while (p.rest != null) {
            ret++;
            p = p.rest;
        }
        return ret;
    }

递归版

public int size(){
    if(this.rest == null)
       return 1;
    return 1 + this.rest.size();
}

🈲退出循环的条件不能改为this == null ,因为如果this为空,那上一层调用size()方法的就是空指针,会收到NULLPointer的错误!

4.获取指定index的元素

编写一个方法:返回列表第i项的item(下标从0开始)

public int get(int index) {
        if (index < 0 || index >= this.size()) {
            return -1;
        } else {
            IntList p = this;
            while (index != 0) {
                p = p.rest;
                index--;
            }
            return p.first;
        }
    }

实际上,上述的IntList是不太好的。

  1. 增加结点,get,size()方法都有可能用到递归。使其他的程序员难以理解。所以我们将构建一个新的类SLList[单链表]
  2. 每个结点的数据域的名字竟然是first,很容易让人误解成首结点。next结点也被命名为rest。

🔵IntList→SLList

1. 改名(将IntList改为IntNode,将数据域改成item,将下一个结点名改为next

public class IntNode {
    public int item;
    public IntNode next;

    public IntNode(int i, IntNode n) {
        item = i;
        next = n;
    }
}

2. 重新创建一个SLList的单独类,让用户与这个类交互

成员变量:头结点

构造函数:创建一个IntNode结点

public class SLList(){

     public IntNode first;

     public SLList(int x){
     IntNode first = new IntNode(x,null);
   }
}

✅在利用构造函数创建新SLList节点的时候不需要传入next指针为空(封装了一层)。只需要传入数据域即可,如下例。

//原方法
IntList node1 = new IntList(5,null);
//现方法
SLList node1 = new SLList(5);

3.增添addFirst&getFirst

SLLis有了first(头指针)的成员变量,所以添加头节点很容易

public void addFirst(int x){
   first = new IntNode(x,first);
}

获取头节点也很容易

public int getFirst(){
    return first.item;
}

4.构建链表5→10→15

从后往前构建,利用我们刚刚写的addFirst函数.左侧为修改后,右侧为修改前。

SLList List = new SLList(15);
List.addFirst(10);
List.addFirst(5);
IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);

5.嵌套类

IntNode类和SLList类放在两个.java文件中实际是没有必要的。IntNode实际上是为了构建SLList而创建的。⇒ 所以我们可以将IntNode类的声明嵌套在SLList类的声明中。

public class IntNode {
    public int item;
    public IntNode next;

    public IntNode(int i, IntNode n) {
        item = i;
        next = n;
    }
}
public class SLList(){
     //嵌套类
     public class IntNode {
         public int item;
         public IntNode next;

         public IntNode(int i, IntNode n) {
		        item = i;
		        next = n;
		    }
		 }
    //自己的成员变量和构造函数
     public IntNode first;
     public SLList(int x){
     IntNode first = new IntNode(x,null);
   }
}

6.声明为静态变量

因为IntNode这个嵌套类并不需要使用SLList中的任何方法和变量,所以我们可以将IntNode声明为static 。改成 public static class IntNode ,IntNode将无法访问SLList中的所有方法和变量。

💡如果内部的类不需要使用外部类的成员和方法,尽量使其成为静态的。

7. addLast( )&size( )

addLast()

public void addLast(int x){
   IntNode p = first;
   while(p.next!=null){
     p = p.next; 
  }
  p。next = new IntNode(x,null);
}

size()递归实现(因为是嵌套类,所以不能在原函数上递归,需要再创建基于IntNode的递归函数。

public int size(){
   return NodeSize(first);
}
public int NodeSize(IntNode p){
   if(p.next==null){
    return 1;  
  }
   return 1+NodeSize(p.next);
}

8.以空间换时间→创建变量size

如果要统计链表的长度,size()方法需要遍历整个链表,当链表很长的时候,耗时很大。所以我们在SLList中增加一个成员变量size。

在每次增加结点时size++,在删除结点时size-1.

9.空链表造成的空指针问题

在封装了嵌套类后,我们创建一个空链表(无参的构造函数)将非常容易

public SLList (){
    first = null;
    size = 0;
}

但如果我们这个空链表增加一个元素(使用addLast方法),会出现null指针异常

public void addLast(int x) {
    size += 1;
    IntNode p = first;
  //p本来就是null指针,p.next就出现访问错误了。
    while (p.next != null) {
        p = p.next;
    }

    p.next = new IntNode(x, null);
}

10.哨兵结点

哨兵结点の数据域:随便保存一个值

哨兵结点のnext指针:指向实际的第一个结点

包含项目 5、10 和 15 的 SLList  将如下所示:

Untitled 5.png