现在准备开始看数据结构了, 不使劲补补是不行的, 顺便也打打基础, 看看内部原理, 争取能到LeetCode上边去刷点题目.
先从最简单的开始, 没有一上来就找一本巨著看, 而是找了一本数据结构与抽象:Java语言描述(原书第4版), 这本看了一下不是非常硬核, 上来就搞那么强的理论. 书的配套源码在此.
趁着京东编程书打折, 买了一本C++ Primer Plus, 准备把C++的部分也看一下, 毕竟耗子哥也说了C++去看一下泛型了解一下, 可以触类旁通.
设计包的接口
包这种数据类型指的是没有顺序, 可以持有重复的对象的数据集合. 为了达到这个目的, 设计的包接口如下:
public class Bag<T> implements BagInterface<T> { @Override public int getCurrnetSize() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean add(T newEntry) { return false; } @Override public T remove() { return null; } @Override public boolean remove(T anEntry) { return false; } @Override public void clear() { } @Override public int getFrequencyOf(T anEntry) { return 0; } @Override public boolean contains(T anEntry) { return false; } @Override public T[] toArray() { return null; } }
用数组实现包 – 定长数组
由于数组一般是一个编程语言最根本的数据结构, 因此一开始尝试使用这个方法来实现.
首先要考虑的是一组核心方法, 核心方法如下:
- 构造器和引入数组相关的变量
public boolean add(T newEntry)
public T[] toArray()
private boolean isArrayFull()
最先来考虑的是, 由于我们需要在内部使用数组, 因此必须在内部先定义一些和数组相关的域, 比如一个数组变量, 数组的大小, 以及包中放了多少东西.
public class ArrayBag<T> implements BagInterface<T> { private final T[] bag; private int numberOfEntries; private static final int DEFAULT_CAPACITY = 25; @Override public int getCurrnetSize() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean add(T newEntry) { return false; } @Override public T remove() { return null; } @Override public boolean remove(T anEntry) { return false; } @Override public void clear() { } @Override public int getFrequencyOf(T anEntry) { return 0; } @Override public boolean contains(T anEntry) { return false; } @Override public T[] toArray() { return null; } }
常见的做法是, 先标明实现接口, 然后添加域, 之后将所有方法都先实现成默认的, 可以通过IDE工具辅助.
这里IDEA会提示数组类型的变量没有初始化, 可能会造成问题, 确实如此, 我们要在构造器中初始化这个变量.
这里首先要知道的就是, Java的数组不支持泛型, 所以要声明成一个Object[]类型的数组, 然后进行类型转换, 不过这会引起编译警告, 因此可以在构造方法上加上压制编译警告的注解.
先来编写构造方法:
/** * 指定长度的包对象的构造器 * * @param capacity int类型数值, 指包的大小 */ @SuppressWarnings("unchecked") public ArrayBag(int capacity) { bag = (T[]) new Object[capacity]; numberOfEntries = 0; } /** * 无参构造方法, 使用默认的数值DEFAULT_CAPACITY=25创建包对象 */ public ArrayBag() { this(DEFAULT_CAPACITY); }
这里IDEA会提示注解和下边的要匹配, 如果没有unchecked错误, 则会提示无需注解.
构造方法结束之后, 我们的包对象在每次实例化的时候,内部已经有了一个数组. 然后就需要来考虑实现核心方法了, 首先就是add方法.
add(T newEntry)方法
这里有一大问题, 就是如何将元素添加到数组中, 是简单的按照索引添加吗. 如果仅仅增大索引, 判断索引是否超过最大数量-1, 就会有一个问题, 即某个位置的元素如果删除了怎么办? 索引就会失效.
所以在考虑add方法的时候, 其实也要考虑remove方法.
这里给出的方法是, 每次添加完成的时候, 都要递增numberOfEntries的值, 下一次添加就将元素放到数组对应numberOfEntries的索引上. 每次删除完成的时候, numberOfEntries递减, 还需要将数组最后一个值放到被删除的位置, 然后将数组最后一个值置为null.
这样就可以保证两种操作都可以正常工作, 由于包是无序的, 因此目前的手段可以满足要求.
根据这个思路, 先来编写add方法:
@Override public boolean add(T newEntry) { boolean isAddSuccess = true; if (isArrayFull()) { isAddSuccess = false; }else { bag[numberOfEntries] = newEntry; numberOfEntries++; } return isAddSuccess; } private boolean isArrayFull() { return numberOfEntries >= bag.length; }
add方法中用到了一个isArrayFull()方法, 这个方法没有必要开放给外界, 因此作为一个私有方法, 只需要判断当前数组中的项目是不是大于等于bag的长度即可, 小于就说明还没有满, 大于和等于就说明已经满了.
public T[] toArray()方法
这个方法看上去很简单, 既然私有变量已经是一个T[]类型, 是不是可以直接返回? 答案是不行, 因为Java返回的是引用, 只要有这个引用, 就可以任意操作其中的数据. 会破坏我们原来的结构.
所以需要采取的方法就是新创建一个数组, 然后将私有变量bag中的每一项复制进去就可以了.
@Override @SuppressWarnings("unchecked") public T[] toArray() { T[] result = (T[]) new Object[bag.length]; //这里IDE提示可以使用System.arraycopy方法 for (int i = 0; i < numberOfEntries; i++) { result[i] = bag[i]; } return result; }
这里如果将返回的数组中的任何一项设置为null, 就不会影响我们内部的私有变量bag对于原来对象的引用. 唯一的缺点依然持有原来每一项的引用, 如果直接更改, 那是没有办法的.
这里真正彻底解决问题的方法是深复制一份, 不过现在还用不到这种.
然后顺便把剩下几个小方法实现一下:
@Override public int getCurrnetSize() { return numberOfEntries; } @Override public boolean isEmpty() { return numberOfEntries == 0; } @Override public void clear() { for (int i = 0; i < numberOfEntries; i++) { bag[i] = null; } numberOfEntries = 0; }
关于clear()方法, 由于数组持有的是引用, 因此还是必须要把所有的引用都设置为null.
到目前为止还有四个方法: 两个remove, 查找某一项的数量, 查找是否包含某一项, 下边就来实现, 不过现在已经可以来测试一下了.
public class OnlineShopper { public static void main(String[] args) { Item[] items = { new Item("Bird feeder", 2050), new Item("Squirrel guard", 1547), new Item("Bird bath", 4499), new Item("Sunflower Seeds", 1295), }; ArrayBag<Item> shoppingCart = new ArrayBag<>(); int totalCost = 0; for (Item nextItem : items) { shoppingCart.add(nextItem); totalCost += nextItem.getPrice(); } System.out.println(Arrays.toString(shoppingCart.toArray())); //这里不能直接使用Item[]类型的数组变量来接着toArray()的结果, 必须如此使用,否则会类型错误 Object[] itemObjects = shoppingCart.toArray(); ((Item) itemObjects[0]).setName("gugugu"); System.out.println(Arrays.toString(shoppingCart.toArray())); } }
通过这段代码可以发现, 还是可以更改其中的数据. 这一点要注意, 没有完全的封装, 但是作为一个数据结构来说已经OK了.
查找项目
前边remove方法的思路已经知道了, 不过在删除之前, 很显然, 还需要先知道一个项目到底存在不存在于包内.
很显然, 要查找一个项目是不是在其中, 需要遍历, 然后调用其中存放的元素的.equals方法.
因此可以写出下列查找是否存在于包内的contains方法:
@Override public boolean contains(T anEntry) { boolean found = false; int index = 0; for (; index < numberOfEntries; index++) { if (bag[index].equals(anEntry)) { found = true; break; } } return found; }
这个方法还是非常简明扼要的, 由于包中可以有重复的元素, 因此找到一个就可以终止查找并返回true了.
然后还有一个方法, 就是返回包中某个项目存在了几次, 这个只要稍作修改, 遍历所有变量然后添加一个计数即可:
@Override public int getFrequencyOf(T anEntry) { int result = 0; for (int index = 0; index < numberOfEntries; index++) { if (bag[index].equals(anEntry)) { result++; } } return result; }
这个方法也很简单, 只不过必定会遍历整个数组, 以免漏掉任何一项.
这里一开始我没有在Item中覆盖标准的equals方法导致自己的equals方法无法调用. 修改后的Item类如下:
public class Item { private int price; private String name; public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Item() { } public Item(String name, int price) { this.price = price; this.name = name; } @Override public String toString() { return "Item{" + "price=" + price + ", name='" + name + '\'' + '}'; } @Override public boolean equals(Object obj) { if (obj.getClass() == Item.class) { return ((Item) obj).getName().equals(this.name) && ((Item) obj).getPrice() == this.price; } else { return false; } } }
现在都成功了, 然后就是关键的remove方法了.
remove方法
remove方法的原理已经知道了, 就是删除之后, 将数组的最后一个数据项填充到刚才删除的位置, 因为包无序, 所以可以不用关心顺序.
首先我们来看T remove()
方法, 调用这个方法会从包里删除一个元素, 只要包不为空, 就可以删除.
在内部, 我们决定从数组的最大一项开始删除. 在每次删除之前, 肯定要检查当前的数组是不是为空, 不是空就暂时存储最大一个元素, 然后将私有变量bag对应的索引设置为空, 再返回最大元素, 所以编写方法如下:
@Override public T remove() { T result = null; if (!this.isEmpty()) { result = bag[numberOfEntries - 1]; bag[numberOfEntries - 1] = null; numberOfEntries--; } return result; }
然后就是remove(T anEntry)
这个删除指定项的方法. 假如这个项目重复, 则我们每次就删除遇到的一个. 这样问题就简化很多.
这个方法需要先判断包是不是为空, 为空就返回false. 如果不为空则需要查找, 查找之后删除, 因此稍微复杂一点:
@Override public boolean remove(T anEntry) { if (!this.isEmpty()) { for (int index = 0; index < numberOfEntries; index++) { if (anEntry.equals(bag[index])) { bag[index] = bag[numberOfEntries - 1]; bag[numberOfEntries - 1] = null; numberOfEntries--; return true; } } } return false; }
如果为空, 就返回false, 如果不为空, 一项一项查找, 找到之后就用最后一项替代刚才那一项, 然后立刻返回true, 不再执行.
实际上到了这里方法就全部写完了, 不过注意观察, 其中的一些代码, 和remove()非常类似. 可以考虑重构一下.
重构remove系列方法
通过观察, 我们可以知道, 如果一个项目位于bag的最后, 则删除这个元素和调用remove()方法没有什么区别.
因此, 核心是获取要删除的元素的索引. 所以我们可以内部定义一个私有方法, 即通过一个元素的索引来删除元素. 新的方法是 private T removeEntry(int index)
假如有了这个方法, 那么public T remove()
方法可以简化成这样:
@Override public T remove() { return removeEntry(numberOfEntries - 1); }
而如果再有一个获取索引的方法, 则可以简化public boolean remove(T anEntry)
方法如下:
@Override public boolean remove(T anEntry) { int index = getIndexOf(anEntry); return removeEntry(index) != null; }
所以下边的关键就是来实现这两个方法:
首先是int getIndexOf(T entry)
, 这里我们要思考, 如果找不到应该返回什么, 0? 正数? 很显然需要返回一个找到的情况下绝对不会出现的数字, 比如可以返回-1:
private int getIndexOf(T entry) { int index = -1; for (int i = 0; i < numberOfEntries; i++) { if (bag[i].equals(entry)) { index = i; break; } } return index; }
有了这个方法之后, 再来编写private T removeEntry(int index)
方法, 就简单多了:
private T removeEntry(int index) { if (index == -1 || isEmpty()) { return null; } T result = bag[index]; bag[index] = bag[numberOfEntries - 1]; bag[numberOfEntries - 1] = null; numberOfEntries--; return result; }
这个方法也很简单, 如果包空或者找不到, 就返回null, 否则返回result. 这样两个remove方法都重构过了, 看看还有没有其他的方法也可以依赖于这两个方法.
答案就是contains, 只需要获取要查找的项目的索引, 然后检查是不是小于等于0就可以了. 所以也可以重构如下:
@Override public boolean contains(T anEntry) { return getIndexOf(anEntry) >= 0; }
使用两个私有方法重构之后, 代码的逻辑就更加清晰了, 主要逻辑核心都变成了数组的索引操作, 这也是数组使用的一大好处. 最后放上完整的代码.
public class ArrayBag<T> implements BagInterface<T> { private T[] bag; private int numberOfEntries; private static final int DEFAULT_CAPACITY = 25; @SuppressWarnings("unchecked") public ArrayBag(int capacity) { bag = (T[]) new Object[capacity]; numberOfEntries = 0; } public ArrayBag() { this(DEFAULT_CAPACITY); } @Override public int getCurrnetSize() { return numberOfEntries; } @Override public boolean isEmpty() { return numberOfEntries == 0; } @Override public boolean add(T newEntry) { boolean isAddSuccess = true; if (isArrayFull()) { isAddSuccess = false; }else { bag[numberOfEntries] = newEntry; numberOfEntries++; } return isAddSuccess; } private boolean isArrayFull() { return numberOfEntries >= bag.length; } @Override public void clear() { for (int i = 0; i < numberOfEntries; i++) { bag[i] = null; } numberOfEntries = 0; } @Override @SuppressWarnings("unchecked") public T[] toArray() { T[] result = (T[]) new Object[bag.length]; for (int index = 0; index < numberOfEntries; index++) { result[index] = bag[index]; } return result; } @Override public int getFrequencyOf(T anEntry) { int result = 0; for (int index = 0; index < numberOfEntries; index++) { if (bag[index].equals(anEntry)) { result++; } } return result; } @Override public boolean contains(T anEntry) { return getIndexOf(anEntry) >= 0; } @Override public T remove() { return removeEntry(numberOfEntries - 1); } @Override public boolean remove(T anEntry) { int index = getIndexOf(anEntry); return removeEntry(index) != null; } private T removeEntry(int index) { if (index == -1 || isEmpty()) { return null; } T result = bag[index]; bag[index] = bag[numberOfEntries - 1]; bag[numberOfEntries - 1] = null; numberOfEntries--; return result; } private int getIndexOf(T entry) { int index = -1; for (int i = 0; i < numberOfEntries; i++) { if (bag[i].equals(entry)) { index = i; break; } } return index; } }
通过看包的第一种实现, 发现这本书写的还挺易懂, 后边进度就要快一些了, 博客也不会记录这么详细了.