Loading

1.数组

《玩转数据结构》-liuyubobobo 课程笔记

数组

把数组码成一排进行存放

数组最好用于索引有语意的情况。

数组最大的优点:快速查询

二次封装自己的数组(动态数组)

我们需要实现:

  1. 实现数组的基本操作,包括增、删、查、改等方法;
  2. 使用泛型,使得数组可以放置“任何”数据类型;
  3. 实现动态数组,使数组的容量可伸缩;

完成数组的增删改查(静态数组)

首先我们完成数组的增删改查等基础功能

简单封装数组

/**
 * 动态数组
 * @author 肖晟鹏
 * @email 727901974@qq.com
 * @date 2021/3/4
 */
public class MyArray<T> {
    private T[] data;
    /**
     * 容量
     */
    private int capacity;

    /**
     * 表示数组中有多少个元素,指向第一个没有元素的位置
     */
    private int size;

    /**
     * 构造函数,传入容量capacity
     * @param capacity
     */
    public MyArray(int capacity){
        this.capacity=capacity;
        this.size=0;
        data=(T[]) new Object[capacity];
    }

    /**
     * 无参构造函数
     */
    public MyArray(){
        this(10);
    }

    /**
     * 构造函数,根据传入数组进行初始化
     * @param data
     */
    public MyArray(T[] data){
        this.data=data;
        this.capacity=data.length;
        this.size=data.length;
    }

    /**
     * 获取元素个数
     * @return
     */
    public int getSize(){
        return size;
    }

    /**
     * 获取数组容量
     * @return
     */
    public int getCapacity(){
        return capacity;
    }

    /**
     * 判断数组是否为空
     * @return
     */
    public boolean isEmpty(){
        return size==0;
    }

    /**
     * 在最后一个位置添加元素
     * @param item
     */
    public void addLast(T item) {
        if (this.size == this.capacity) {
            throw new IllegalArgumentException("AddLast failed.MyArray is full.");
        }
        this.data[size] = item;
        this.size++;
    }

    /**
     * index要大于0并且小于size
     * @param index 索引
     */
    public void checkIndex(int index){
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Index is illegal.Require index >= 0 and index < size.");
        }
    }

    @Override
    public String toString() {
        StringBuilder res=new StringBuilder();
        //%d是一个占位符
        res.append("MyArray:size="+this.size+",capacity="+this.capacity+"\n");
        res.append("[");
        for(int i=0;i<size;i++){
            res.append(this.data[i].toString());
            if(i!=size-1){
                res.append(",");
            }
        }
        res.append("]");
        return res.toString();
    }
}

在指定位置插入元素

如果我们想在数组索引为1的位置插入一个元素(数组size>1),我们怎么做呢?
我们可以把数组索引为1和之后的元素向后挪一个位置,把位置腾出来,然后再把元素插入进去。
在具体挪的时候,要从后往前挪。如图,将100移到size指向的位置,再移动99...
移动之后,维护size(size++)

    /**
     * 在最后一个位置添加元素
     * O(1)
     * @param item
     */
    public void addLast(T item) {
        add(size,item);
    }

    /**
     * 在第一个位置添加元素
     * O(n)
     * @param item
     */
    public void addFirst(T item){
        add(0,item);
    }

  /**
     * 在指定地方插入元素
     * 因为不知道具体index的数量,所以视为平均数2/n
     * 因为计算大O的时候,需要忽略常数
     * O(n/2) = O(n)
     * @param index
     * @param item
     */
    public void add(int index, T item) {
        
        //index要大于0并且小于size,因为元素要紧密相连
        this.checkIndex(index);
        
        if (this.size == this.capacity) {
            throw new IllegalArgumentException("Add failed.MyArray is full.");
        }
        

        for (int i = size - 1; i >= index; i--) {
            this.data[i + 1] = this.data[i];
        }
        this.data[index] = item;
        size++;
    }

修改元素

   /**
     * 获取元素
     * @param index
     * @return
     */
    public T get(int index) {
        this.checkIndex(index);
        return this.data[index];
    }

    /**
     * 修改元素
     * @param index
     * @param item
     */
    public void set(int index, T item) {
        this.checkIndex(index);
        this.data[index] = item;
    }

包含和搜索

将数组进行遍历,如果找到了,就return一个指定的值

    /**
     * 是否包含某元素
     * O(n)
     * @param item
     * @return
     */
    public boolean contains(T item) {
        for (T i : this.data) {
            if (Objects.equals(item, i)) {
                return true;

            }
        }
        return false;
    }

    /**
     * 查找元素所在索引,若没有找到则返回-1
     * O(n)
     * @param item
     * @return
     */
    public int find(T item) {
        for (int i = 0; i < size; i++) {
            if (Objects.equals(item, this.data[i])) {
                return i;
            }
        }
        return -1;
    }

其中Objects.equals是jdk的工具类里的方法:

package java.util;


public final class Objects {
    ...
    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
    ...
}

删除元素

删除时,移动元素,从前面到后面,将索引为2的赋值给索引1,然后再将3赋值给2,这样一直到size-1(最后一个元素),最后维护size,size--。

问题:size是表示数组中有多少个元素,指向第一个没有元素的位置,但是以上这样有影响吗?

答:没有,因为用户无法获得索引为size的元素,因为我们进行了合法性判断,index必须大于等于0并且小于size的。其实在Java语言中,数组只要开辟了空间,辣么都是有默认值的。

    /**
     * 删除指定位置的元素
     * O(n/2) = O(n)
     * @param index
     * @return 返回删除的元素
     */
    public T remove(int index) {
        //index 合法性判断
        checkIndex(index);
        T res = this.data[index];
        for (int i = index + 1; i < size; i++) {
            this.data[i - 1] = data[i];
        }
        size--;

        //将引用置空,好让垃圾回收机制发生
        data[size] = null;
        return res;
    }

    /**
     * 删除第一个元素
     * O(n)
     * @return
     */
    public T removeFirst() {
        return remove(0);
    }

   /**
     * 删除最后一个元素
     * O(1)
     * @return
     */
    public T removeLast() {
        return remove(size - 1);
    }

    /**
     * 删除已有的元素,包括重复的元素
     * @param item
     * @return 进行了删除操作返回true,没有则返回false
     */
    public boolean removeElement(T item) {
        boolean falg=true;
        boolean res=false;
        int index;
        while (falg){
            index=find(item);
            if (index == -1) {
                falg=false;
            }else {
                remove(index);
                res=true;
            }
        }
        return res;
    }

动态数组

以上我们都还是使用的是JAVA的静态数组,不能满足我们的需求,我们需要一个可伸缩的数组。

原理:创建一个新的更大的数组,将原来的数组装入新数组

将data中的所有成员放到newData中,并且将data指向新的数组。原数组因为没有被引用了,所以会被Java的自动回收机制回收,newData在方法结束后就失效了,最终我们会得到一个更大的data

    /**
     * 对数组进行扩容,用户不需要知道怎么扩容或者缩容的,所以我们这里设置为private
     * @param newCapacity
     */
    private void resize(int newCapacity){
        T[] newData=(T[]) new Object[newCapacity];
        for (int i=0;i<size;i++){
            newData[i]=data[i];
        }
        this.data = newData;
        this.capacity = newCapacity;
    }

我们到这里就可以思考一下,之前我们添加元素的时候,当元素装满了我们就抛出异常,辣么我们现在是不是就可以在其内部进行扩容了呢?如下:

   /**
     * 缩容比例
     */
    private final int RESIZE_SHRINK = 2;

    /**
     * 判断缩容的条件
     */
    private final int RESIZE_IS_SHRINK = 4;

    /**
     * 扩容比例
     */
    private final int RESIZE_EXPANSION = 2;

...
    
    public void add(int index, T item) {

        //index要大于0并且小于size,因为元素要紧密相连
        this.checkIndex(index);

        //动态扩容
        //使用定义的常量,避免魔法值
        if (this.size == this.capacity) {
            resize(this.RESIZE_EXPANSION * capacity);
        }
        ...
    }

...

    public T remove(int index) {
        //index 合法性判断
        checkIndex(index);

        //使用Lazy策略,防止复杂度震荡
        //使用定义的常量,避免魔法值
        if(size <= capacity/this.RESIZE_IS_SHRINK && capacity/this.RESIZE_SHRINK !=0){
            resize(capacity/this.RESIZE_SHRINK);
        }
        ...
    }

注意,这里删除的时候使用了Lazy策略,这里是为了解决复杂度震荡的问题,即:数组在临界状态下,反复扩容和缩容。

如果数组缩容条件是size <= capacity/2 && capacity/2 !=0 ,有一个数组,其capacity=10,size=9,当增加一个元素的时候,进行扩容,此时其属性为capacity=20,size=10,此时只要进行删除元素,则就会进行缩容。当这个数组反复在size=10size=9得临界情况下摩擦的时候,数组就会反复扩容缩容,这明显降低了数组的性能,所以我们设计,当size <= capacity/4 && capacity/2 !=0的时候进行缩容,即只有当数组size为capacity的4分之一的时候才缩容,并且只缩容一半,这样就避免了其在临界情况下反复扩容缩容的问题。

增加操作:

均摊复杂度:O(n)

最坏复杂度:O(n^2)

但是一般来说,扩容操作绝对不会每次都触发,所以这个时候,计算均摊复杂度更有意义

MyArray

import java.util.Objects;

/**
 * 动态数组
 * @author 肖晟鹏
 * @email 727901974@qq.com
 * @date 2021/3/4
 */
public class MyArray<T> {

    /**
     * 缩容比例
     */
    private final int RESIZE_SHRINK = 2;

    /**
     * 判断缩容的条件
     */
    private final int RESIZE_IS_SHRINK = 4;

    /**
     * 扩容比例
     */
    private final int RESIZE_EXPANSION = 2;

    private T[] data;
    /**
     * 容量
     */
    private int capacity;

    /**
     * 表示数组中有多少个元素,指向第一个没有元素的位置
     */
    private int size;

    /**
     * 构造函数,传入容量capacity
     * @param capacity
     */
    public MyArray(int capacity){
        this.capacity=capacity;
        this.size=0;
        data=(T[]) new Object[capacity];
    }

    /**
     * 无参构造函数
     */
    public MyArray(){
        this(10);
    }

    /**
     * 构造函数,根据传入数组进行初始化
     * @param data
     */
    public MyArray(T[] data){
        this.data=data;
        this.capacity=data.length;
        this.size=data.length;
    }

    /**
     * 获取元素个数
     * @return
     */
    public int getSize(){
        return size;
    }

    /**
     * 获取数组容量
     * @return
     */
    public int getCapacity(){
        return capacity;
    }

    /**
     * 判断数组是否为空
     * @return
     */
    public boolean isEmpty(){
        return size==0;
    }


    /**
     * index要大于0并且小于size
     * @param index 索引
     */
    public void checkIndex(int index){
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Index is illegal.Require index >= 0 and index < size.");
        }
    }

    /**
     * 对数组进行扩容,用户不需要知道怎么扩容或者缩容的,所以我们这里设置为private
     * @param newCapacity
     */
    private void resize(int newCapacity){
        T[] newData=(T[]) new Object[newCapacity];
        for (int i=0;i<size;i++){
            newData[i]=data[i];
        }
        this.data = newData;
        this.capacity = newCapacity;
    }

    //============================增加元素=============================

    /**
     * 在最后一个位置添加元素
     * O(1)
     * @param item
     */
    public void addLast(T item) {
        add(size,item);
    }

    /**
     * 在第一个位置添加元素
     * O(n)
     * @param item
     */
    public void addFirst(T item){
        add(0,item);
    }

    /**
     * 在指定地方插入元素
     * 因为不知道具体index的数量,所以视为平均数2/n
     * 因为计算大O的时候,需要忽略常数
     * O(n/2) = O(n)
     * @param index
     * @param item
     */
    public void add(int index, T item) {

        //index要大于0并且小于size,因为元素要紧密相连
        this.checkIndex(index);

        //动态扩容
        //使用定义的常量,避免魔法值
        if (this.size == this.capacity) {
            resize(this.RESIZE_EXPANSION * capacity);
        }

        for (int i = size - 1; i >= index; i--) {
            this.data[i + 1] = this.data[i];
        }
        this.data[index] = item;
        size++;
    }

    //============================增加元素END==========================


    //============================修改元素=============================

    /**
     * 获取元素
     * @param index
     * @return
     */
    public T get(int index) {
        this.checkIndex(index);
        return this.data[index];
    }

    /**
     * 修改元素
     * @param index
     * @param item
     */
    public void set(int index, T item) {
        this.checkIndex(index);
        this.data[index] = item;
    }

    //============================增加元素END==========================

    //============================查找元素=============================

    /**
     * 是否包含某元素
     * O(n)
     * @param item
     * @return
     */
    public boolean contains(T item) {
        for (T i : this.data) {
            if (Objects.equals(item, i)) {
                return true;

            }
        }
        return false;
    }

    /**
     * 查找元素所在索引,若没有找到则返回-1
     * O(n)
     * @param item
     * @return
     */
    public int find(T item) {
        for (int i = 0; i < size; i++) {
            if (Objects.equals(item, this.data[i])) {
                return i;
            }
        }
        return -1;
    }

    //============================查找元素END==========================


    //============================删除元素=============================

    /**
     * 删除指定位置的元素
     * O(n/2) = O(n)
     * @param index
     * @return 返回删除的元素
     */
    public T remove(int index) {
        //index 合法性判断
        checkIndex(index);

        //使用Lazy策略,防止复杂度震荡
        //使用定义的常量,避免魔法值
        if(size <= capacity/this.RESIZE_IS_SHRINK && capacity/this.RESIZE_SHRINK !=0){
            resize(capacity/this.RESIZE_SHRINK);
        }

        T res = this.data[index];
        for (int i = index + 1; i < size; i++) {
            this.data[i - 1] = data[i];
        }
        size--;

        //将引用置空,好让垃圾回收机制发生
        data[size] = null;
        return res;
    }

    /**
     * 删除第一个元素
     * O(n)
     * @return
     */
    public T removeFirst() {
        return remove(0);
    }

    /**
     * 删除最后一个元素
     * O(1)
     * @return
     */
    public T removeLast() {
        return remove(size - 1);
    }

    /**
     * 删除已有的元素,包括重复的元素
     * @param item
     * @return 进行了删除操作返回true,没有则返回false
     */
    public boolean removeElement(T item) {
        boolean falg=true;
        boolean res=false;
        int index;
        while (falg){
            index=find(item);
            if (index == -1) {
                falg=false;
            }else {
                remove(index);
                res=true;
            }
        }
        return res;
    }


    //============================删除元素END==========================




    @Override
    public String toString() {
        StringBuilder res=new StringBuilder();
        //%d是一个占位符
        res.append("MyArray:size="+this.size+",capacity="+this.capacity+"\n");
        res.append("[");
        for(int i=0;i<size;i++){
            res.append(this.data[i].toString());
            if(i!=size-1){
                res.append(",");
            }
        }
        res.append("]");
        return res.toString();
    }

}
posted @ 2021-03-25 17:46  硝酸铜  阅读(77)  评论(0)    收藏  举报