美文网首页
数据结构1-数组_不要小瞧数组_冒泡、选择、插入、归并、快速、随

数据结构1-数组_不要小瞧数组_冒泡、选择、插入、归并、快速、随

作者: 小安的大情调 | 来源:发表于2019-12-31 18:14 被阅读0次

努力努力再努力xLg

前言
数组在Java中算是耳熟能详的了。但是在数据结构方面,它是一个非常基础,并且非常有意义的数据结构,接下来我们将一步一步,从易到难的研究所有在Java中运用到的数据结构,所带来的便利、和魅力。

不要小瞧了数组

​ 在Java中提供的数组属于静态数组。不可动态扩容,在初始化时,需要指定大小或者容积。今天基于Java的静态数组,来实现一组动态数组。体会一下,不容小觑的数组-数据结构。

​ 众所周知,在数组中,最大的一个特性就是在每一个元素下,都有属于自己的索引。

​ 那么索引,在数组中,可以是有语义的,也可以没有语义;

有语义:可以用数组的索引代表排名。这样就体现得更加直观。

那么索引给数组带来了那些好处呢?

​ 根据索引,查找数据非常快,时间复杂度为(O(1)级别)。但是在增删操作中,就显得力不从心了,因为在增删操作中需要额外的维护原有的数组的索引,并且需要维护数组的大小。在这里考虑到极端情况下,需要扩容。时间复杂度则为O(n)

代码逻辑

package ch1_array.ch1;

import java.util.Objects;

/**
 * 不要小瞧数组
 * 自定义,动态数组
 *
 * @author by Mr. Li
 * @date 2019/12/31 1:07
 */
public class MyArray<E> {
    /**
     * 初始容量 data
     */
    private E[] data;
    /**
     * 当前数组中含有多少元素
     */
    private int size;

    /**
     * 构造函数:定义一个capacity大小的数组
     *
     * @param capacity 默认数组的容量
     */
    public MyArray(int capacity) {
        data = (E[]) new Objects[capacity];
        size = 0;
    }

    /**
     * 无参构造:定义一个默认容量为8 的数组
     */
    public MyArray() {
        this(8);
    }

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

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

    /**
     * 获取数组的容量
     *
     * @return Capacity
     */
    public int getCapacity() {
        return data.length;
    }

    /**
     * 查看数组中是否包含该元素
     *
     * @param e
     * @return
     */
    public boolean contains(E e) {
        for (int i = 0; i < size; i++) {
            if (e == data[i]) {
                return true;
            }
        }
        return false;
    }

    /**
     * 在数组中查找某个元素
     *
     * @param e 需要查找的元素
     * @return 返回该元素的索引位置,如果没有则返回-1
     */
    public int find(E e) {
        for (int i = 0; i < size; i--) {
            if (data[i] == e) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 修改 某索引的元素
     *
     * @param index
     * @param e
     */
    public void set(int index, E e) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed.index should is index < 0 || index >= size");
        }
        data[index] = e;
    }

    /**
     * 根据索引 获取元素
     *
     * @param index 索引
     * @return 该元素
     */
    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed.index should is index < 0 || index >= size");
        }
        return data[index];
    }

    public void add(E e) {
        add(size, e);
    }

    public E getLast() {
        return get(size - 1);
    }

    public E getFirst() {
        return get(0);
    }

    /**
     * 删除某个某个索引上的元素
     *
     * @param index 指定的索引
     * @return 返回被删除的元素
     */
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Remove failed.index should is index < 0 || index >= size");
        }
        E res = data[index];
        for (int i = index; i < size - 1; i++) {
            data[i] = data[i + 1];
        }
        size--;
        // 考虑到内存优化问题,当数组内元素减少到一定程度时。进行缩容。
        // 并且考虑到时间复杂度,最坏的情况下,如果在2倍的零界点一直做addLast和removeLast操作会使
        // 两个时间复杂度为O(1)级别的操作变成了O(n)。
        // 所以考虑到上述问题,在缩容时,我们要考虑到,容量确实没有必要那么大的时候,再进行缩容。
        if (size <= getCapacity() / 4 && getCapacity() / 4 >= 8) {
            // 当当前元素小于容积的4分之一时,进行缩容,但是容易不可以为0,所以这里控制最小容量为8
            resize(getCapacity() / 2);
        }
//        优化代码,将该位置指向空白地址,供垃圾回收机制回收。
        data[size] = null;
        return res;
    }

    public E removeFirst() {
        return remove(0);
    }

    public E removeLast() {
        return remove(size - 1);
    }

    /**
     * 移除某个元素
     *
     * @param e
     */
    public void removeElement(E e) {
        int index = find(e);
        if (index != -1) {
            remove(index);
        }
    }

    /**
     * 添加元素
     *
     * @param index 添加元素的 索引位置
     * @param e     添加的具体元素
     */
    public void add(int index, E e) {
        // 判断index 是否超过当前容量
//        if (size == getCapacity()) {
//            throw new IllegalArgumentException("Add failed. Array is full.");
//        }
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed.Require index >=0 || index <= size");
        }
        // 使用动态扩容的技术,实现动态数组
        if (size == getCapacity()) {
            resize(getCapacity() * 2);
        }
        // 将添加的元素,添加到数组中,应该从后往前,遍历到需要插入的索引位置,并且遍历到的元素,都需要往后移一个位置
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        // 将新元素插入到数组中
        data[index] = e;
        // 维护 数组大小
        size++;
    }

    /**
     * 在最后索引处 添加元素
     *
     * @param e 新元素
     */
    public void addLast(E e) {
        add(size, e);
    }

    /**
     * 在数组的头添加元素
     *
     * @param e
     */
    public void addFirst(E e) {
        add(0, e);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 0; i < size; i++) {
            sb.append(data[i]);
            if (i != size - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }

    /**
     * 实现动态数组,扩容!
     */
    private void resize(int newCapacity) {
        E[] newData = (E[]) new Objects[newCapacity];
        // 复制数组中的元素到新的数组中
        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }
}

所有的代码逻辑都非常清楚的写在代码注解中。

动态数组

​ 这里需要特别重点的说一下扩容与缩容,在动态数组中的重要地位。

在上述代码中我们看到add操作为在容量不足时,将数组容量扩容到当前容量的两倍。

我们来假设一种非常极端的情况。

​ 在对数组进行操作时,如果刚好处在扩容的最后一个元素,在进行addLast操作,需要进行扩容,与此同时,在缩容时,也是在当前size时容积的一半时,进行缩容,那么问题就来了,就在刚刚做了扩容的时候,再删除一个元素,马上又要进行缩容,如此反复,时间复杂度成质数倍增长。可想而知带来的性能消耗有多大。所以这样的操作对于JVM来说太过于激进。

​ 所以这里需要对缩容采取Lazy的方式,当程序发现确实有很大一部分空间被浪费时,再进行缩容,就如上述代码中,当Size为容积的1/4时再进行缩容为容积的1/2。这样程序就现得更加的合理,和健壮了。

使用数组实现各种排序

冒泡排序

​ 比较相邻的两个元素,按需将大/或者小(升降序决定)往后移。一个一个遍历如下图


冒泡排序.gif

代码:

    public static int[] arr = {55, 4, 7, 56, 89, 21, 2, 3, 0};

    @Test
    public void test_1() {
        // 冒泡排序
        int temp;
        // 升序
        for (int i = 0; i < arr.length; i++) {

            for (int j = 0; j < arr.length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) { // 在这里升序降序改一下大于 小于就可以了
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        Arrays.stream(arr).forEach(System.out::println);
    }

选择排序

​ 将数组中所有元素一一对比,每一次遍历选出所有为遍历中的之最。则为选择排序

选择排序.gif
    @Test
    public void test_2() {
        int temp;
        int index;
        for (int i = 0; i < arr.length; i++) {
            index = i;
            temp = arr[i];
            for (int j = i + 1; j < arr.length; j++) {
                if (temp > arr[j]) {
                    temp = arr[j];
                    // 记录最小的值的索引。
                    index = j;
                }
            }
            arr[index] = arr[i];
            arr[i] = temp;
        }
        Arrays.stream(arr).forEach(System.out::println);
    }

插入排序

​ 通常将第一个元素作为参考值,从第二个元素开始和这个参考值进行比较,比这个参考值大的时候放在这个参考值的后面,比这个参考值小的时候在和这个参考值的前一位进行比较,当比较至适当位置进行插入。


插入排序.gif
    @Test
    public void test_3() {
        int temp;
        for (int i = 0; i < arr.length - 1; i++) {
            if (arr[i + 1] < arr[i]) {
                for (int j = i + 1; j > 0; j--) {
                    if (arr[j] < arr[j-1]){
                        temp = arr[j];
                        arr[j] = arr[j - 1];
                        arr[j - 1] = temp;
                    }
                }
            }
        }
        Arrays.stream(arr).forEach(System.out::println);
    }

归并排序

​ 归并排序利用的是分治的思想实现的,对于给定的一组数据,利用递归与分治技术将数据序列划分成为越来越小的子序列,之后对子序列排序,最后再用递归方法将排好序的子序列合并成为有序序列。合并两个子序列时,需要申请两个子序列加起来长度的内存,临时存储新的生成序列,再将新生成的序列赋值到原数组相应的位置。

归并排序.gif
这里录制的GIF有问题,看后面加速的则是归并排序
    /**
     * 归并排序
     */
    @Test
    public void test_4() {
        merSort(arr, 0, arr.length - 1);
        Arrays.stream(arr).forEach(System.out::println);
    }

    public static void merSort(int[] arr, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            merSort(arr, left, mid);//左边归并排序,使得左子序列有序
            merSort(arr, mid + 1, right);//右边归并排序,使得右子序列有序
            merge(arr, left, mid, right);//合并两个子序列
        }
    }

    private static void merge(int[] arr, int left, int mid, int right) {
        //也可以从开始就申请一个与原数组大小相同的数组,因为重复new数组会频繁申请内存
        int[] temp = new int[right - left + 1];
        int i = left;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= right) {
            if (arr[i] < arr[j]) {
                temp[k++] = arr[i++];
            } else {
                temp[k++] = arr[j++];
            }
        }
        while (i <= mid) {//将左边剩余元素填充进temp中
            temp[k++] = arr[i++];
        }
        while (j <= right) {//将右序列剩余元素填充进temp中
            temp[k++] = arr[j++];
        }
        //将temp中的元素全部拷贝到原数组中
        for (int k2 = 0; k2 < temp.length; k2++) {
            arr[k2 + left] = temp[k2];
        }
    }

快速排序

​ //TODO

参考书籍

本文仅供本人学习,一起进步!

相关文章

  • 数据结构1-数组_不要小瞧数组_冒泡、选择、插入、归并、快速、随

    努力努力再努力xLg 前言数组在Java中算是耳熟能详的了。但是在数据结构方面,它是一个非常基础,并且非常有意义的...

  • 排序算法

    冒泡排序 选择排序 插入排序 归并排序 快速排序 数组内置方法

  • 排序算法

    常见的排序算法 常见的排序算法有:插入、希尔、选择、冒泡、归并、快速、堆排序。。。 插入排序 算法步骤一、从数组的...

  • 面试问题总结

    数组排序 桶、堆、冒泡、基数、归并、插入、快速、选择1:桶排序 2:冒泡排序 3:选择排序思想:把最小的放在第一位...

  • 算法总结

    一,排序算法:冒泡排序,选择排序,快速排序,归并排序,插入排序,堆排序,希尔排序冒泡排序:会重复的比较数组中相邻的...

  • JavaScript 排序算法

    目录 冒泡排序 插入排序 选择排序 快速排序 归并排序 堆排序 桶排序 冒泡排序 思路 1相邻数值比较2找出数组最...

  • 排序

    冒泡 插入 选择 谢尔 归并 快速

  • 学习算法第三天 —— 排序

    题目:对数组进行排序冒泡排序插入排序快速排序归并排序 题目 对一个数组进行排序,所有的方法: 冒泡排序 时间复杂度...

  • JavaScript实现排序算法

    实现了冒泡,选择,插入,快排,希尔,归并 冒泡排序 选择排序 插入排序 快速排序 希尔排序 归并排序

  • swift中几种排序算法原理的UI动态实现

    swift中的排序算法总结 冒泡排序 选择排序 快速排序 插入排序 堆排序 归并排序 系统排序 我们将这几种数组排...

网友评论

      本文标题:数据结构1-数组_不要小瞧数组_冒泡、选择、插入、归并、快速、随

      本文链接:https://www.haomeiwen.com/subject/rflioctx.html