动态数组原理【Java实现】(六)
前言
接下来我们进入集合学习,看过很多文章一上来就是讲解原理感觉会特别枯燥,任何成熟解决方案的出现都是为了解决问题,若通过实际问题引入然后再来讲解原理想必学起来必定事半功倍,从我写博客的那一天起,我就在思考如何通过通俗易懂的话让看到文章的童鞋立马能明白我讲解的什么,即使文章很长若是层层递进定不会感到枯燥乏味,所以我脑海里一直在高度不停旋转着去找合适的例子。关于集合学习将分为例子引入、源码分析、数据结构分析三个部分来进行阐述。
集合入门
我们以一个例子来进行集合入门讲解,上学时我们上体育课,首先体育老师会叫我们多少多少同学站成一对来进行报名,然后自由解散休息,哈哈。体育老师要求我们站成一对,这个时候就好对数组进行数据存储,我们假设体育老师需要5个人站成一对,所以这也就对应数组初始化的容量,有了容量之后,接下来则是在所要求的地方站到指定位置,这就好比往数组里添加元素,好了接下来我们以Java语言来实现这一要求。首先我们定义一个排队类,依据面向对象封装思想,我们对外提供操作方法,所以在此类中定义私有的数组,然后定义集合中元素个数属性,如下:
public class QueueDemo { private int Size; private Integer[] Elements; }
依据我们所分析,我们按照5人站成一对,所以当我们初始化排队类时就初始化数组容量,也就是说我们在构造函数中初始化容量,如下:
public class QueueDemo { // 数组大小 private int Size; // 数组 private Integer[] Elements; // 初始化数组容量 public QueueDemo(int capacity) { Elements = new Integer[capacity]; } }
我们初始化容量后呢,接下来则是对应同学开始排队,此时也就是对应往数组里添加元素,所以我们封装一个添加方法,每添加一个元素则数组大小则递增1,如下:
// 添加元素 public void Add(Integer element) { Elements[Size] = element; Size++; }
对应每一步操作,我们都遍历打印出数组中元素,所以接下来我们重写toString方法,如下:
// 重写toString打印元素 @Override public String toString() { int length = Elements.length; StringBuilder sb = new StringBuilder(); sb.append("["); for (int i = 0; i < length; i++) { sb.append(Elements[i]); if (i != length - 1) { sb.append(","); } } return sb.toString(); }
我们完成了同学排队第一步,接下来我们实例化上述排队类并添加元素(我们将元素看做时排队时同学们的姓名),最后打印元素,如下:
public class Main { public static void main(String[] args) { QueueDemo demo = new QueueDemo(5); demo.Add(1); demo.Add(2); demo.Add(3); demo.Add(4); demo.Add(5); System.out.println(demo); } }
当同学们排成一队后,体育老师发现排队的同学高矮不一,然后将同学与同学之间按照高矮进行调换,这也就对应着我们需要封装插入元素的方法,因为我们初始化数组容量为5,当我们在指定索引插入一个元素时,再打印元素必然抛出数组异常,也就是涉及到数组容量扩容,我们暂且定义在指定索引插入元素的方法,如下:
public void Insert(int index, Integer element) { }
当按照高矮排好队后,体育老师认为一列只需排4个人,剩余一个人到其他对去,这也就对应删除数组中的元素,同样我们定义删除方法,当我们删除数组中元素时,需要将删除的元素后的元素都往前移一位,同时将最后一位置为空,数组大小也减少一位,如下:
// 删除元素 public void Remove(Integer element) { int index = GetIndex(element); for (int i = index; i < Elements.length - 1; i++) { Elements[i] = Elements[i + 1]; } Elements[Size - 1] = null; Size--; }
接下来我们再来删除并打印元素,如下:
//删除元素 demo.Remove(3); System.out.println(demo);
因为我们将数组最后一位元素置为空,所以在打印时应删除,我们继续改造重写的toString方法,如下:
@Override public String toString() { int length = Elements.length; StringBuilder sb = new StringBuilder(); sb.append("["); for (int i = 0; i < length; i++) { if (Elements[i] == null) { continue; } sb.append(Elements[i]); if (i != length - 1) { sb.append(","); } } if (sb.charAt(sb.length() - 1) == ',') { sb.delete(sb.length() - 1, sb.length()); } sb.append("]"); return sb.toString(); }
接下来体育老师要求报数,比如根据某个同学的姓名即元素报出自己所在的第几位(也就对应数组中的索引),所以此时我们再封装一个获取指定元素的索引方法,如下:
// 获取指定元素索引 public int GetIndex(Integer element) { for (int i = 0; i < Elements.length - 1; i++) { if (!Elements[i].equals(element)) { continue; } return i; } throw new RuntimeException("未找到"); }
然后我们尝试获取4号同学所排队的位置是,如下:
System.out.println("4号同学所在的位置是 :" + demo.GetIndex(4));
到了这里我们完成了排队的基本要求,但是还远远不够,比如我们是自定义初始化容量,这里我们指定为5,经过删除操作后,最终数组中存在4个元素,要是我们再往数组中添加至少2个以上元素,此时打印数组元素将抛出异常,所以这里为了解决这个问题我们对数组进行自动扩容,也就是对添加方法进行改造,当添加元素时我们需要判断是否已经超过数组容量,若超过,我们将数组容量扩大到现有数组容量的2倍,那么我们应该怎么判断呢?我们通过数组大小和数组容量进行判断,如下:
public void Add(Integer element) { if (Size >= Elements.length) { Elements = Arrays.copyOf(Elements, 2 * Elements.length); } Elements[Size] = element; Size++; }
public static void main(String[] args) { QueueDemo demo = new QueueDemo(5); demo.Add(1); demo.Add(2); demo.Add(3); demo.Add(4); demo.Add(5); System.out.println(demo); //删除元素 demo.Remove(3); System.out.println(demo); System.out.println("4号同学所在的位置是 :" + demo.GetIndex(4)); demo.Add(6); demo.Add(7); System.out.println(demo);
}
在排队时我们给定人数为5,也就说数组初始化容量为5,这还不够灵活,如果体育老师已经明确规定一列必须站几个,我们直接就能接受到体育老师规定的信号,这就像明确指定了数组的初始化容量,这样一来既能保证不会抛出异常,同时也不会影响当添加和插入元素时扩容时所带来的性能开销,如果未明确规定一列站几个,我们也可以默认初始化容量,如此最灵活,一切都未变且性能最佳,如下我们定义一个默认初始化容量并改造排队列构造函数,如下:
private int DEFAULT_CAPACITY = 10; public QueueDemo() { Elements = new Integer[DEFAULT_CAPACITY]; } public QueueDemo(int capacity) { Elements = new Integer[capacity <= 0 ? DEFAULT_CAPACITY : capacity]; }
到了这里我们还未实现在指定索引位置插入元素的Insert方法,既然要插入指定索引位置,首先我们必须检查指定索引位置是否超过数组大小,然后将指定索引后的元素向后移动一位,最后留出指定索引位置进行插入,如下:
public void Insert(int index, Integer element) { if (Size <= index || index < 0) { throw new RuntimeException("超出数组边界"); } System.arraycopy(Elements, index, Elements, index + 1, Size - index); System.out.println(this); Elements[index] = element; Size++; }
QueueDemo demo = new QueueDemo(); demo.Add(1); demo.Add(2); demo.Add(3); demo.Add(4); demo.Add(5); System.out.println(demo); //删除元素 demo.Remove(3); System.out.println(demo); System.out.println("4号同学所在的位置是 :" + demo.GetIndex(4)); demo.Add(6); demo.Add(7); System.out.println(demo); //插入元素 demo.Insert(2, 20); System.out.println(demo);
如上我们首先检查指定索引是否小于0或者是否超出数组大小,否则抛出异常,然后这里我们通过内置提供的方法,从指定索引位置后的元素进行复制即Index+1,最后复制元素的长度为Size-Index。此时指定索引位置数据仍为4,最后我们将指定索引位置的值通过我们要插入的值进行替换。
总结
如上则是我们实现比较完整的排队需求,当然还有一些参数检查的小问题,看到这里想必很多童鞋就已经清楚知道了,其实我们实现的就是Java中的集合,有了本节课的基础,下节课我们进行ArrayList源码分析将得心应手,感谢阅读,我们下节见。