Java数据结构基础 ——— 详解顺序表,链表

目录

一、顺序表

1. 概念及结构

2.接口实现

1.在POS位置新增元素

2.打印顺序表

 3.获取顺序表的有效值个数

4.判断是否包涵某个元素

5.在顺序表任意位置插入元素

6.查找某个元素对应的位置

7.获取index位置的元素

8.给index位置值设置为val

9.删除第一个出现的key元素

10.清空顺序表

二、无头单向链表

1.链表概念及结构

2.单向无头链表及其实现

1.链表节点包含的成员

 2.头插法

3.尾插法 

 4.任意位置插入

 5.判断链表是否包含关键字key

6.删除第一个元素为key的节点

7.删除所有值为key的节点

8.清空链表

3.链表与顺序表的异同

1.顺序表优缺点

2.链表优缺点:


一、顺序表

1. 概念及结构

 顺序表是用一段 物理地址连续 的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数 据的增删查改。
顺序表一般可以分为:
1.静态顺序表:使用定长数组存储。
 2.动态顺序表:使用动态开辟的数组存储。
 静态顺序表适用于确定知道需要存多少数据的场景 .。静态顺序表的定长数组导致N 定大了,空间开多了浪费,开少了不够用 . 相比之下动态顺序表更灵活, 根据需要动态的分配空间大小。

2.接口实现

我们首先新建一个Java文件,定义好一个公共类:该类当中包含两个成员:1.一个数组elem  2.一个整形size,代表该数组内的有效元素个数。

同时,我们定义一个构造方法,构造这个类的时候我们将先前定义的成员数组赋予10的长度。

public class MyLinearList {
    public int[] elem;
    int size;

    public MyLinearList() {
        this.elem = new int[10];
    }
}

这样,我们就完成了顺序表的基础工作。下面,我们来实现顺序表的一些接口方法。

1.在POS位置新增元素

这其实就是一个插入新元素的方法:

   public void add(int data) {
        if(this.size == elem.length) {
            elem = Arrays.copyOf(elem,2 * this.elem.length);
        }
        this.elem[this.size] = data;
        this.size++;
    }

在实现该方法的时候应考虑到数组是否还有多余的空间?当有效元素个数size等于数组长度的时候,我们为了可以插入新的元素就需要增加数组的长度。

2.打印顺序表

该方法要求将顺序表内的有效元素按顺序打印在屏幕上,因此我们只需要遍历顺序表即可。

    public void print() {
        for (int i = 0; i < this.size; i++) {
            System.out.print(this.elem[i] + " ");
        }
        System.out.println();
    }

我们写完了两个基础的接口,下面来运行一下看看:

public static void main(String[] args) {
        MyLinearList a = new MyLinearList();
        a.add(0,1);
        a.add(1,2);
        a.add(2,3);
        a.add(3,4);
        a.add(4,5);
        a.print();
    }

我们在主函数里定义一个顺序表,然后插入五个元素打印看看。

运行结果:

 3.获取顺序表的有效值个数

每次新增元素或者删除元素时,我们都会对size进行相关操作,因此只需要返回size便可。

    public int getSize() {
        return this.size;
    }

4.判断是否包涵某个元素

该方法只需要遍历顺序表便可,如果发现该元素便返回true,如果循环结束还没有找到,返回false。

    public boolean contain(int key) {
        for (int x: this.elem) {
            if(x == key)
                return true;
        }
        return false;
    }

5.在顺序表任意位置插入元素

该方法需要将该位置及后面的有效元素往后挪动一位,需要注意的是应当从后往前挪。

除此之外,还应当考虑顺序表是否需要扩容、插入位置是否合法。

   public void addAnyWhere(int index, int data) {
        if(index < 0 || index > this.size) {
            System.out.println("位置不合法");
        }
        if(this.size == this.elem.length) {
            this.elem = Arrays.copyOf(this.elem, 2 * elem.length);
        }
        for(int i = this.size; i > index; i--) {
            this.elem[i] = this.elem[i - 1];
        }
        this.elem[index] = data;
        this.size++;
    }

6.查找某个元素对应的位置

该方法只需要遍历顺序表,发现目标元素后返回其下标便可。

   public int search(int key) {
        for(int i = 0; i < this.size; i++) {
            if(this.elem[i] == key) {
                return i;
            }
        }
        return -1;
    }

7.获取index位置的元素

首先判断输入位置是否合法,如果不合法则提示位置不合法,也可以抛异常。

   public int getKey(int index) {
        if(index < 0 || index >= this.size) {
            System.out.println("位置不合法!");
        }
        return this.elem[index];
    }

8.给index位置值设置为val

与上一个方法类似,就不赘述了,以下是代码:、

   public void setKey(int index, int key) {
        if(index < 0 || index >= this.size) {
            System.out.println("位置不合法!");
        }
        this.elem[index] = key;
    }

9.删除第一个出现的key元素

该方法的实现是遍历列表,当发现第一个与key元素时将后面的元素向前覆盖,最后size减一即可。

注意:覆盖顺序必须是从前往后。

   public void deleteKey(int key) {
        int i = 0;
        for(i = 0; i < this.size; i++) {
            if(this.elem[i] == key) {
                break;
            }
        }
        for(; i < this.size - 1; i++) {
            this.elem[i] = this.elem[i+1];
        }
        this.size--;
    }

10.清空顺序表

只需要将顺序表元素挨个置0,最后将size也置0便可。

实现代码:

    public void empty() {
        for (int i = 0; i < this.usedSize; i++) {
            this.elem[i] = 0;
        }
        this.usedSize = 0;
    }

以上就是一些顺序表的基础方法的实现。

二、无头单向链表

上半节我们实现了顺序表及其增删查改的一些基本方法,那么同样是基础的数据结构,链表相对于顺序表都有哪些不同?二者分别又有什么优势和缺陷?

1.链表概念及结构

我们知道,顺序表本质是数组,在数组的基础上我们可以进行增删查改,数组内储存的各元素的地址在物理储存结构上是连续的,而下面我们要说的链表并不是依靠连续的储存地址来将各个节点连接在一起,它的连接是依靠引用链接实现的。

链表大致可以分为四类:1.带头单向链表  2.不带头单向链表  3.带头双向链表  4.不带头双向链表。当然,也有循环和非循环之分,算上这两个特征,链表的种类可以扩大到八种之多。

下面我们要实现的是单向的无头链表和一些它的增删查改的方法。


2.单向无头链表及其实现

1.链表节点包含的成员

根据我们一开始对链表的描述以及先前对顺序表的讲解,我们应当可以理解到链表节点应该包涵两部分:1.储存的数值  2.下一个节点的引用。

class ListNode {
    public int val;//储存的数值
    public ListNode next;//下一个节点的引用

    public ListNode(int val) {//含一个参数的构造方法
       this.val = val;
    }
}

在上面的代码中,我们创建了一个节点的类,这个类当中除了我们一开始提到的两个成员外,还有一个构造方法,所以我们每次在构造节点的时候都可以对里面储存的数字进行初始化,而指向下一个节点的引用我们保持默认值:null。

定义完这些我们就可以在主函数创建一个链表了:

   public static void main(String[] args) {
        ListNode a1 = new ListNode(1);
        ListNode a2 = new ListNode(2);
        ListNode a3 = new ListNode(3);
        ListNode a4 = new ListNode(4);
        a1.next = a2;
        a2.next = a3;
        a3.next = a4;
    }

 我们实例化了四个节点,这四个节点按顺序赋值为1到4,我们可以观察到,从a1开始,每个节点的next都指向了下一个节点,链表也是这样连接起来的。最后a4则不指向任何对象,它是这条链表的最后一个节点。

我们尝试写一段打印节点元素的代码看看:

   public static void main(String[] args) {
        ListNode a1 = new ListNode(1);
        ListNode a2 = new ListNode(2);
        ListNode a3 = new ListNode(3);
        ListNode a4 = new ListNode(4);
        a1.next = a2;
        a2.next = a3;
        a3.next = a4;
        while(a1 != null) {
            System.out.print(a1.val);
            a1 = a1.next;
        }
    }

进入循环后都会打印当前节点储存的值,打印完了之后a1便指向它的下一个节点,周而复始。直到a1指向a4,而a4的下一个节点是null,下一次无法进入循环,打印结束。我们也是用这样的方法去遍历链表的。但要注意:当前代码只是演示所用,一般我们是不会去随便改变头节点的指向对象的。

运行后:

 2.头插法

先前我们的代码中创建完节点后还需要一个一个地连接起来,十分地麻烦。下面我们来实现一个方法,在创建节点的同时还能将节点相连。

头插法是指,我们在插入新定义的节点时,将新的节点放在原链表的头结点之前,而那个新的节点也将成为新的头结点。

在此之前我们定义一个公共类,我们可以将链表的方法写在该类当中方便调用,并示例化一个节点作为头结点。

public class MyLinkedList {
    public ListNode head;


}

下面实现头插法:

    //头插法
    public void addFirst(int val) {
        ListNode node = new ListNode(val);
        if(this.head == null) {//链表为空,第一个插进来的节点就是头结点
            this.head = node;
        } else {
            node.next = this.head;//指向原头结点
            this.head = node;//将新节点定义为头结点
        }
    }

我们在主函数当中使用一下:

    public static void main(String[] args) {
        MyLinkedList my = new MyLinkedList();
        my.addFirst(4);
        my.addFirst(3);
        my.addFirst(2);
        my.addFirst(1);
        my.prain();//打印函数,与之前使用的循环是一个程序
    }

运行结果:

3.尾插法 

上面我们实现了从前面插入节点的头插法,那么假如我们想要在最后插入节点又该如何实现?

首先,我们需要找到链表的最后一个节点,再将最后一个节点的next指向新的节点即可。需要注意的是:如果链表是空的,那么在最后插入就相当于插入头结点。

    //尾插法
    public void addLast(int val) {
        ListNode node = new ListNode(val);
        if(this.head == null) {//判断链表是否为空
            this.head = node;
        } else {
            ListNode cur = this.head;
            while(cur.next != null) {//遍历至最后一个节点
                cur = cur.next;
            }
            cur.next = node;
        }
    }

在主函数上使用一下:

    public static void main(String[] args) {
        MyLinkedList my = new MyLinkedList();
        my.addFirst(4);
        my.addFirst(3);
        my.addFirst(2);
        my.addFirst(1);
        my.addLast(5);
        my.addLast(6);
        my.prain();
    }

运行结果:

 4.任意位置插入

假如我们已经创建完一个链表了,在实际运用的时候我们有时候需要插入新的节点在链表的中间。这是头插法和尾插法无法做到的,所以我们需要一个新的方法(头节点下标为0以此类推)。

方法实现思路:1.找到要插入的位置(实际上需要找到的是插入位置的前一个节点)2.新节点的next指向原节点的next 3.将前一个节点的next指向新节点

根据上面的思路,这个方法是需要使用到双指针的(既要找到插入位置的前一个节点,又要找到插入位置的节点)

    public void addIndex(int index, int val) {
        if(index < 0 || index > size()) {//size()为链表长度方法,与打印函数思想一致
            return;
        }
        if(index == 0) {//头插法
            addFirst(val);
            return;
        }
        if(index == size()) {//尾插法
            addLast(val);
            return;
        }
        ListNode node = new ListNode(val);//创建新节点
        ListNode cur = this.head;
        ListNode pre = new ListNode(0);
        pre.next = cur;
        for(int i = 0; i < index; i++) {
            cur = cur.next;//找到插入位置节点
            pre = pre.next;//找打插入位置的前一个节点
        }
        pre.next = node;//前一个节点的next指向新节点
        node.next = cur;//新节点的next指向原位置节点,那么新节点就插入成功了
    }

我们来使用一下:

   public static void main(String[] args) {
        MyLinkedList my = new MyLinkedList();
        my.addFirst(4);
        my.addFirst(3);
        my.addFirst(2);
        my.addFirst(1);
        my.addLast(5);
        my.addLast(6);
        my.addIndex(0,0);
        my.addIndex(4,0);
        my.addIndex(8,0);
        my.prain();
    }

运行结果:

 5.判断链表是否包含关键字key

该方法只需要历遍链表元素即可,在此就不做赘述。

实现代码:

    public boolean contains(int key) {
        ListNode cur = this.head;
        while(cur != null) {
            if(cur.val == key) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

6.删除第一个元素为key的节点

实现思路:1.遍历链表找到第一个值为key的节点  2.将该节点的前一个节点的next指向它的next

实现代码:

    public void remove(int key) {
        if(this.head == null) {
            return;
        }
        if(this.head.val == key) {//头结点为要删除的节点
            this.head = this.head.next;
            return;
        }
        ListNode cur = this.head;
        ListNode pre = new ListNode(0);
        pre.next = cur;
        while(cur != null) {
            if(cur.val == key) {//找到值为key的节点
                pre.next = cur.next;
                break;
            }
            cur = cur.next;
            pre = pre.next;//pre永远指向cur的前一个节点
        }
        //没找到
    }

在这个方法的实现当中,头结点较为特殊(它是没有前一个节点的),删除头结点的方法就是将head指向当前头结点的next。当没有任何引用指向该对象时就会被系统回收。

7.删除所有值为key的节点

实现方法与前一个类似,唯一区别是不论找到与否,都要遍历至最后一个节点。

实现代码:

      //删除所有值为key的节点
      public void removeall(int key) {
        if(this.head == null) {
            return;
        }
        if(this.head.val == key) {//头结点为要删除的节点
            while(this.head.val == key) {//循环结束代表头结点值不等于key
                this.head = this.head.next;
            }
        }
        ListNode cur = this.head;
        ListNode pre = new ListNode(0);
        pre.next = cur;
        while(cur != null) {
            if(cur.val == key) {//找到值为key的节点
                pre.next = cur.next;
                cur = cur.next;
                if(cur == null) {
                    break;
                }
            }
            cur = cur.next;
            pre = pre.next;//pre永远指向cur的前一个节点
        }
        //没找到
    }

8.清空链表

清空链表只需要将链表各节点的next逐个置空即可

实现代码:

    public void clear() {
        ListNode cur = this.head.next;
        while(cur != null) {
            this.head.next = null;
            this.head = cur;
            cur = cur.next;
        }
        this.head = null;
    }

到这里链表及其基础方法都已经实现完了。

3.链表与顺序表的异同

1.顺序表优缺点

优点:

1.存取速度快,通过下标直接获取元素。

2.空间连续。

缺点:

1. 顺序表中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

2.链表优缺点:

优点:

1.任意位置插入删除时间复杂度为O(1) 。

2.没有增容问题,插入一个开辟一个空间。

缺点:

以节点为单位存储,不支持随机访问 。
  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值