开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情
学习恋上数据结构与算法笔记
1.什么是数据结构?
数据结构是计算机存储、组织数据的方式
在实际应用中,根据使用场景来选择最合适的数据结构
2.线性表
线性表是具有个相同类型元素的有限序列()
是首节点(首元素), 是尾结点(尾元素) 是 的前驱, 是 的后继
常见的线性表有:
- 数组
- 链表
- 栈
- 队列
- 哈希表(散列表)
3.数组(Array)
数组是一种顺序存储的线性表,所有元素的内存地址是连续的
int[] array = new int[]{11, 22, 33};
在很多编程语言中,数组都有个致命的缺点:无法动态修改容量
实际开发中,我们更希望数组的容量是可以动态改变的
4.动态数组(Dynamic Array)接口设计
| 函数 | 说明 |
|---|---|
int size(); | 元素的数量 |
boolean isEmpty(); | 是否为空 |
boolean contains(E element); | 是否包含某个元素 |
void add(E element); | 添加元素到最后面 |
E get(int index); | 返回index位置对应的元素 |
E set(int index, E element); | 设置index位置的元素 |
void add(int index, E element); | 往index位置添加元素 |
E remove(int index); | 删除index位置对应的元素 |
int indexOf(E element); | 查看元素的位置 |
void clear(); | 清除所有元素 |
编程思想
尽量避免魔数,静态常量单独拿出来声明,字母大写,中间下划线连接。
private static final int DEFAULT_CAPACITY = 10; // 默认容量
private static final int ELEMENT_NOT_FOUND = -1; // 没有找到
构造器之间使用
this来复用代码,为了增强鲁棒性,给capacity初始值如果小于10就默认设置为10
public ArrayList(int capacity) {
// 如果初始容量小于10则默认认为初始容量为10
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[]) new Object[capacity];
}
public ArrayList() {
// elements = new int[DEFAULT_CAPACITY];
this(DEFAULT_CAPACITY);
}
如果使用
get取元素的时候下标越界,直接抛出异常即可。方便定位错误位置。
public E get(int index) {
// 下标越界
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size:" + size);
}
return elements[index];
}
size = 0可以表示清空,当然也可以置为null和重新new但是那样效率太低了,总而言之,实现的方式很多,需要具体情况具体分析。这就相当于是设计一个框架,内部的实现对外部是不可见的,我们是内部的实现。
public void clear() {
// 即使后面有数据也访问不到
size = 0;
}
5. 动态数组的设计
在Java中,成员变量会自动初始化,比如int类型自动初始化为 0,对象类型自动初始化为 null
5.1 添加元素 - add(E element)
后续可以优化为直接调用后面已经写好的方法add(index, ele),
5.2 打印数组
重写toString方法
在 toString方法中将元素拼接成字符串
字符串拼接建议使用
StringBuilder
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
for (int i = 0; i < size; i++) {
if(i != 0) {
string.append(", ");
}
string.append(elements[i]);
// if(i != size - 1) {
// string.append(", ");
// }
}
string.append("]");
return string.toString();
}
推荐使用if(i != 0)的写法,原因是效率较高,相比于下面的写法不用多做一次减法运算size - 1。
5.3 删除元素 - remove(int index)
思考:最后一个元素如何处理? 不需要处理 比如这里剩下的 77,因为size--了,无法访问到最后一个元素了
数组的内存分配是连续的,只能是数组里面的元素往前挪动。
5.4 添加元素 - add(int index, E element)
注意挪动的顺序,与添加元素是相反的。
public void add(int index, int element) {
if(index < 0 || index > size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
for (int i = size - 1; i >= index; i--) {
elements[i + 1] = elements[i];
}
elements[index] = element;
size++;
}
注意这里的判断条件是index > size表示可以在最后一个位置上插入元素list.add(list.size(), 100);,
由于代码中有很多对于size的判断,属于重复代码,将这一部分进行封装:
/**
* 封装同样的异常
*
* @param index 索引
*/
private void outOfBounds(int index) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
/**
* 常规范围检查
*
* @param index
*/
private void rangeCheck(int index) {
if (index < 0 || index >= size) {
outOfBounds(index);
}
}
/**
* 添加范围检查
*
* @param index
*/
private void rangeCheckForAdd(int index) {
if(index < 0 || index > size) {
outOfBounds(index);
}
}
接口测试:
public class Asserts {
public static void test(boolean value) {
try {
if (!value) throw new Exception("测试未通过");
} catch (Exception e) {
e.printStackTrace();
}
}
}
调用Asserts.test(list.get(3) == 80);进行测试
5.5 如何扩容
重新申请一块更大的内存空间。
注意这里不能用size和capacity进行比较,因为size存储的是具体元素有多少个,而应该用此时数组能够存储的容量elements.length和capacity进行比较
5.6 泛型
如何创建泛型数组?
elements = new E[capacity]; 错误
elements = (E[]) new Object[capacity];
所有的类,最终都继承java.lang.Object类
5.7 对象数组
改为泛型之后多了一个对象的内存管理的问题。如果是new int[10]很明显知道需要分配4 * 10共40个字节的空间,但是如果是对象数组的话,new Object[10]不知道分配的空间?new Object[7]里面存放的是对象的地址值。
每个对象的内存地址大小是一样的,比如说都占8个字节。
5.8 内存管理细节
如何销毁这个对象,把这个线断掉。将对应位置上的值赋为null。
如果栈空间中的objects数组到堆空间中的指向断掉了,那么数组首先会被回收,然后数组中引用的对象被回收。
此时当clear的时候需要回收内存。之前由于数组中的每个元素都是int,是基本类型的数据,无需额外处理,直接size = 0就好,但是如果数组中存储的是对象地址时,这些对象会一直在内存中。
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
clear细节:
/**
* 对象死之前要做什么事情
*
* @throws Throwable
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Person - finalize");
}
// 提醒JVM进行垃圾回收
// 检查哪些对象没有被指向,没有被指向的通通被回收内存
System.gc();
删除remove的细节:
将最后位置上的元素赋值为null,否则该元素仍然被引用,无法被垃圾回收
元素比较是否相等:
重写equals方法
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj instanceof Person) {
Person person = (Person) obj;
return this.age == person.age;
}
return false;
}
修改方法:
/**
* 查看元素的索引
*/
public int indexOf(E element) {
for (int i = 0; i < elements.length; i++) {
if (elements[i].equals(element)) return i;
}
return ELEMENT_NOT_FOUND;
}
5.9 null 的处理
数组中能够存放空元素?这是一个内部设计的问题。
如果数组中运行添加null元素,如果数组中存在多个null元素,那么indexOf方法中存在问题:
/**
* 查看元素的索引
*/
public int indexOf(E element) {
for (int i = 0; i < elements.length; i++) {
// null无法调用equals方法,会报空指针异常
if (elements[i].equals(element)) return i;
}
return ELEMENT_NOT_FOUND;
}
6. java.util.ArrayList
modCount只有在迭代器里面才有用
移除某个元素
public void remove(E element) {
remove(indexOf(element));
}