目录
1.数据结构
1.什么是数据结构?
- 数据结构式计算机存储数据,组织数据的方式。
2.常见的数据结构
- 线性结构:线性表(数组、链表、栈、队列、哈希表)
- 树形结构:二叉树,AVL树,红黑树,B树,堆,Trie,哈弗曼,并查集
- 图形结构:邻接矩阵,邻接表。
3.在实际应用中,根据使用场景来选择最合适的数据结构。
2.线性表概述
线性表是具有n个相同类型元素的有限序列(n >= 0),在逻辑上具有索引和数据部分:是否能通过索引能直接访问到数据部分,要看具体的线性表类型(典型代表:数组和链表)。
常见的概念:
- a1是首节点(首元素),an是尾节点(尾元素)。
- a1是a2的前驱,a2是a1的后继。
常见的线性表有:
- 数组
- 链表
- 栈
- 队列
- 哈希表(散列表)
3.线性表:数组Array
3.1.概述
1.数组是一种书信存储的线性表,所有元素的内存地址是连续的。
2.数组内存分配
我们看一下这句Java代码中,new的int类型数组的内存分配情况:
int[] array = new int[]{
11, 22, 33};
局部变量array放在栈空间中,堆空间中new开辟出来3个int大小的连续
内存空间:
3.Java数组的缺点
在很多编程语言中,数组都有一个明显的缺陷。比如上面的语句:
int[] array = new int[]{
11, 22, 33};
数组array初始化后,其容量大小就是固定好了,以后无法动态修改数组容量
。数组元素存满后,就无法再存放数据了,因为不能扩大数组的大小了。
4.实际开发中,我们更希望数组的容量是可以动态改变的,所以我们会自己实现一个可以动态扩容的数组。
那么我们需要考虑一个问题:我们自己实现的可扩容的数组需要提供哪些API接口给别人调用?
3.2.动态数组接口设计
要满足一个动态数组的功能,那么最起码要提供这些公共的接口:
public class ArrayList {
/**
* 清除所有元素
*/
public void clear() {
}
/**
* 元素的数量
* @return
*/
public int size() {
return 0;
}
/**
* 是否为空
* @return
*/
public boolean isEmpty() {
return false;
}
/**
* 是否包含某个元素
* @param element
* @return
*/
public boolean contains(int element) {
return false;
}
/**
* 添加元素到尾部
* @param element
*/
public void add(int element) {
}
/**
* 获取index位置的元素
* @param index
* @return
*/
public int get(int index) {
return 0;
}
/**
* 设置index位置的元素
* @param index
* @param element
* @return 原来的元素ֵ
*/
public int set(int index, int element) {
return 0;
}
/**
* 在index位置插入一个元素
* @param index
* @param element
*/
public void add(int index, int element) {
}
/**
* 删除index位置的元素
* @param index
* @return
*/
public int remove(int index) {
return 0;
}
/**
* 查看元素的索引
* @param element
* @return
*/
public int indexOf(int element) {
return 0;
}
}
3.3.动态数组的设计实现
1.成员变量
/**
* 元素的数量个数:注意不是数组的容量
*/
private int size;
/**
* 引用:指向一个int数组
*/
private int[] elements;
//默认的数组容量大小
private static final int DEFAULT_CAPACITY = 10;
//-1下标:代表没有这个元素
private static final int ELEMENT_NOT_FOUND = -1;
2.构造方法:
/**
* 构造1:不指定数组容量:默认容量是10
*/
public ArrayList() {
//调用有参构造
this(DEFAULT_CAPACITY);
}
/**
* 构造2:我的设计
* 指定初始容量,如果参数小于默认容量时用默认容量
* @param capacity
*/
public ArrayList(int capacity) {
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = new int[capacity];
}
3.方法实现
- size()
/**
* 元素的数量
* @return 返回size成员变量
*/
public int size() {
return size;
}
2.isEmpty()
/**
* 是否为空
* @return
*/
public boolean isEmpty() {
return size == 0;
}
3.get(int index)
/**
* 获取index位置的元素
* @param index
* @return
*/
public int get(int index) {
if(index < 0 || index > size) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
return elements[index];
}
4.set(int index, int element)
/**
* 设置index位置的元素
* @param index
* @param element
* @return 原来的元素ֵ
*/
public int set(int index, int element) {
if(index < 0 || index > size) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
int old = elements[index];
elements[index] = element;
return old;
}
5.indexOf(int element)
/**
* 查看元素的索引
* @param element
* @return
*/
public int indexOf(int element) {
for (int i=0; i < size; i++) {
if (elements[i] == element) return i;
}
return ELEMENT_NOT_FOUND;
}
6.contains(int element)
/**
* 是否包含某个元素
* @param element
* @return
*/
public boolean contains(int element) {
return indexOf(element) != ELEMENT_NOT_FOUND;
}
- clear()
直接将size设为0,不做具体的“清空”操作,这样反而可能更加高效。因为原数组可能还需要add元素,那么直接覆盖原来废弃的值即可。这样就不用有清空操作,重新申请空间等操作。
框架设计者,自己来设计功能背后的逻辑。只要对外“语义”是通顺的,背后的设计逻辑,由我们自己来决定(考虑到性能方面)。
/**
* 清除所有元素
*/
public void clear() {
size = 0;
}
8.add(int element):暂时不考虑扩容的问题
/**
* 添加元素到尾部
* @param element
*/
public void add(int element) {
elements[size++] = element;
}
9.重写toString():用来打印
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("[");
for (int i = 0; i < size; i++) {
if(i != 0)
string.append(", ");
string.append(elements[i]);
}
string.append("]");
return string.toString();
}
10.remove(int index):
删除index位置的元素:具体操作是index后面的位置前移,覆盖掉要删除的元素。所以数组的删除和插入操作很麻烦。
/**
* 删除index位置的元素:
* 具体操作是index后面的位置前移,覆盖掉要删除的元素。
* 所以数组的删除操作很麻烦。
* @param index
* @return 被删除的元素
*/
public int remove(int index) {
if(index < 0 || index > size) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
int old = elements[index];
//index后的元素前移
while (index < size - 1) {
elements[index] = elements[index+1];
index++;
}
size--;
return old;
}
11.复用出来3个方法
/**
* 检查索引是否越界
* @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);
}
}
/**
* 打印索引越界异常信息
* @param index
*/
private void outOfBounds(int index) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
12.add(int index, int element):在指定位置插入元素
/**
* 在index位置插入一个元素:原来该位置的元素及后面的元素后移
* @param index
* @param element
*/
public void add(int index, int element) {
rangeCheckForAdd(index);
for(int i = size; i > index; i--) {
elements[i] = elements[i-1];
}
elements[index] = element;
size++;
}
3.4.动态扩容
1.实现分析
当数组Array的容量已经填满了,一般数组就不再允许向其中添加,插入元素。如果想要实现,即使数组满了也能向其中插入元素,那么此时就需要对数组进行扩容了。
扩容的实现一般是申请一个更大的内存,将原来的元素赋值到这个新的内存中,原来的引用指向新申请的内存地址。
2.哪些方法需要考虑扩容
重构一下add(int element)和add(int index, int element):
- 既然add(int element)是向尾部插入元素,那么直接调用add(size, element即可,这样只要更新,扩展add(int index, int element)方法,add(int element)机会随之改变。
- 重构代码:
/**
* 添加元素到尾部
* @param element
*/
public void add(int element) {
add(size, element);
}
/**
* 在index位置插入一个元素:原来该位置的元素及后面的元素后移
* @param index
* @param element
*/
public void add(int index, int element) {
rangeCheckForAdd(index);
for(int i = size; i > index; i--) {
elements[i] = elements[i-1];
}
elements[index] = element;
size++;
}
- 加上扩容机制:
/**
* 确保数组容量能满足插入,新增操作:
* 如果满足:不做任何操作
* 否则:扩容为原来的1.5倍
* @param capacity:插入,添加元素时,需要的最小容量
*/
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
//如果旧容量>=需要的最小容量,说明容量足够,可以插入
if (oldCapacity >= capacity) return;
//否则就需要扩容:位运算效率比算数运算高很多
int newCapacity = oldCapacity + (oldCapacity >> 1);
//复制元素到新扩容出来的数组中
int[] newElememts = new int[newCapacity];
for (int i = 0; i < size; i++) {
newElememts[i] = elements[i];
}
elements = newElememts;
System.out.println("扩容了: "+"旧容量,"+oldCapacity+"新容量,"+newCapacity);
}
/**
* 在index位置插入一个元素:原来该位置的元素及后面的元素后移
* @param index
* @param element
*/
public void add(int index, int element) {
rangeCheckForAdd(index);
//确保容量足够:查看容量是否足够,不够就扩容
ensureCapacity(size + 1);
for(int i = size; i > index; i--) {
elements[i] = elements[i-1];
}
elements[index] = element;
size++;
}
4.动态数组实现的完善
4.1.泛型
泛型技术可以让动态数组更加通用,可以存放任何数据类型。
类定义时,指明元素是泛型,其它用到元素类型的地方都改成泛型:
public class ArrayList<E> {
/**
* 构造2:指定初始容量,如果参数小于默认容量时用默认容量
* @param capacity
*/
public ArrayList(int capacity) {
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = new E[capacity];//Cannot create a generic array of E
}
/**
* 确保数组容量能满足插入,新增操作:
* @param capacity:插入,添加元素时,需要的最小容量
*/
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
if (oldCapacity >= capacity) return;
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElememts = new E[newCapacity]; //Cannot create a generic array of E
for (int i = 0; i < size; i++) {
newElememts[i] = elements[i];
}
elements = newElememts;
}
}
注意上面有两个报错://Cannot create a generic array of E,即不能创建泛型数组,那么我们怎么解决呢?
- 首先我们不能写死类型:因为每次创建的元素类型都是不固定的。
elements = new Person[capacity];
elements = new Car[capacity];
- 那么我们要找一个能代表所有类型的类:Object,然后再强转:
elements = (E[])new Object[capacity];
E[] newElememts = (E[])new Object[newCapacity];
4.2.对象数组的内存管理
1.看一下这两个创建数组的语句在内存分配上的区别:
int[] array1 = new int[3];
Object[] array2 = new Object[7]
- 语句1:因为int是固定的4个字节,所以会直接在堆内存中分配4 * 3个字节大小的内存空间。
- 语句2:如果创建的是一个对象(Object对象)数组,那么堆空间中申请的连续7块内存,存放的是地址值。即存放的是7个引用,这样更方便以后指向其他的对象(用到多态时),所以可以存放任何对象元素。
2.clear()方法的含义和改进
clear()方法的含义:是清除元素,但是保留数组的“架子”,这样以后还能重复利用,直接添加元素覆盖。
/**
* 清除所有元素
*/
public void clear() {
size = 0;
}
1.因为以前是new int[n]; 那么堆空间开辟的空间中直接存放的就是int值。设置size=0后,其他的方法用array试,语义上也是符合的。而且如果后面会用到这个空间,直接重复利用就好,不用再重新开辟了。
2.那么现在是泛型了,存储的是对象了,还能这样写吗?
要分析一下:对象数组对空间中存放的是地址值,而且地址值指向了对象元素。那么和以前的int[]数组相比,多出来了的是地址元素指向对象这个部分,其他还是一样的。
那么当我们不想要这些对象,想要清空时,如果还是直接size=0的话,虽然对使用者在使用时来说语义上没有影响,但是这些对象还是存在的,直到地址元素指向一个新的对象,此时原来的对象才算销毁。
考虑到内存利用的情况:我们的clear()方法要主动销毁这些对象。
3.所以还是和new int[n];的情况一样,我们保留数组内存,清除对象内存。
存放地址值的内存可以重复利用的,还可以被覆盖为其他的对象地址,所以不能直接将数组引用置为null:elements = null; 这样就会导致整个数组内存都销毁了,即存放地址值的堆内存空间没有了指向,也会被销毁。下次再用的话,只能重新开辟。
能循环利用的留下,不能循环利用的滚蛋。
4.新的clear()
/**
* 清除所有元素
*/
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
- 测试
public class Person {
/**
* 对象被销毁时:调用(写下遗言)
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("我得遗言:"+"我没了...");
}
}
ArrayList<Person> list = new ArrayList<>();
list.add(new Person(12, "aaaa", new Date()));
list.add(new Person(13, "bbbb", new Date()));
list.add(new Person(14, "cccc", new Date()));
list.clear();
//主动提示GC回收
System.gc();
//结果:
我得遗言:我没了...
我得遗言:我没了...
我得遗言:我没了...
3.remove()方法改进
- 以前的remove(int index):删除位置后的元素前移后,最后一个元素没有处理
public E remove(int index) {
rangeCheck(index);
E old = elements[index];
//index后的元素前移
while (index < size-1) {
elements[index] = elements[index+1];
index++;
}
size--;
return old;
}
- 改进:
public E remove(int index) {
rangeCheck(index);
E old = elements[index];
//index后的元素前移
while (index < size-1) {
elements[index] = elements[index+1];
index++;
}
elements[--size] = null;
return old;
}
- remove(E element):删除指定元素
/**
* 删除指定元素
* @param element
* @return
*/
public int remove(E element) {
int index = indexOf(element);
remove(indexOf(element));
return index;
}
4.3.equals
1.indexOf(E element)方法中涉及到对象的比较,那么就不能直接用“==” 。
“==”比较对象时比较的是两个对象的内存地址,我们在此处自己定义比较规则:两个对象的各个属性一样时,认为相等(即使不是同一个对象)。
假设一个类中重写了自己的比较规则equals,那么对象数组中比较这个对象元素时,也要调用equals方法。
如果Person类没有重写equals方法,ArrayList类indexO()方法仍然调用equals方法,那么会走Object中的equals方法,Object中的equals方法会调用"=="比较。
Integer等包装类,里面已经重写了equals方法,equals方法比较的是数值。
//Person类中重写equals方法
@Override
public boolean equals(Object obj) {
Person person = (Person)obj;
return person.age == this.age && person.name == this.name;
}
/**
* 查找元素的索引
* @param element
* @return
*/
public int indexOf(E element) {
for (int i=0; i < size; i++) {
if (elements[i].equals(element)) return i;
}
return ELEMENT_NOT_FOUND;
}
2.equals的进一步改进
当element[i]可能是Person对象,也可能是Integer对象时,那么每次调用Person的equals接收的obj实际类型可能根本就不是Person。
那么每次直接强转成Person,显然后报错: ClassCastException
改进代码:
@Override
public boolean equals(Object obj) {
if(obj == null) return false;
if(obj instanceof Person) {
Person person = (Person)obj;
return person.age == this.age && person.name == this.name;
}
//否则:接收的obj不是Person类型
return false;
}
4.4.null值处理,是否可以存储空数据
- 一个内部设计方面的问题:是否可以存储null数据。
arrayList.add(null);
- 看一下add()方法,其实我们的实现默认是支持存null的
public void add(int index, E element) {
rangeCheckForAdd(index);
//确保容量足够:查看容量是否足够,不够就扩容
ensureCapacity(size + 1);
for(int i = size; i > index; i--) {
elements[i] = elements[i-1];
}
elements[index] = element;
size++;
}
3.那么此时就又会出现问题:indexOf(E element)
indexOf(E element)方法中的:elements[i].equals(element)判断语句,因为可能存在null元素,那么null.equals()就会报错。
public int indexOf(E element) {
for (int i=0; i < size; i++) {
if (elements[i].equals(element)) return i;
}
return ELEMENT_NOT_FOUND;
}
那么分成两种情况考虑:
/**
* 查找元素的索引
* @param element
* @return
*/
public int indexOf(E element) {
if(element == null) {
for (int i=0; i < size; i++) {
if (elements[i] == null) return i;
}
}else {
//那么element一定不为null,放在前面调用equals绝对没问题
for (int i=0; i < size; i++) {
if (element.equals(elements[i])) return i;
}
}
return ELEMENT_NOT_FOUND;
}
5.整个代码
最后贴一下我整个的代码:
5.1 Person
package com.mj;
import java.util.Date;
public class Person {
private int age;
private String name;
private Date birthday;
public Person(int age, String name, Date birthday) {
this.age = age;
this.name = name;
this.birthday = birthday;
}
@Override
public String toString() {
return "Person [age=" + age + ", name=" + name + ", birthday=" + birthday + "]";
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public Person() {
super();
}
@Override
public boolean equals(Object obj) {
if(obj == null) return false;
if(obj instanceof Person) {
Person person = (Person)obj;
return person.age == this.age && person.name == this.name;
}
//否则:接收的obj不是Person类型
return false;
}
/**
* 对象被GC回收时:调用(写下遗言)
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("我的遗言:"+"我没了...");
}
}
5.2.com.mj.ArrayList
package com.mj;
@SuppressWarnings("unchecked")
public class ArrayList<E> {
/**
* 元素的数量
*/
private int size;
/**
* 所有的元素
*/
private E[] elements;
//默认的数组容量大小
private static final int DEFAULT_CAPACITY = 10;
//-1下标:代表没有这个元素
private static final int ELEMENT_NOT_FOUND = -1;
/**
* 检查索引是否越界
* @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);
}
}
/**
* 打印索引越界异常信息
* @param index
*/
private void outOfBounds(int index) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
/**
* 确保数组容量能满足插入,新增操作:
* 如果满足:不做任何操作
* 否则:扩容为原来的1.5倍
* @param capacity:插入,添加元素时,需要的最小容量
*/
private void ensureCapacity(int capacity) {
int oldCapacity = elements.length;
//如果旧容量>=需要的最小容量,说明容量足够,可以插入
if (oldCapacity >= capacity) return;
//否则就需要扩容:位运算效率比算数运算高很多
int newCapacity = oldCapacity + (oldCapacity >> 1);
//复制元素到新扩容出来的数组中
E[] newElememts = (E[])new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElememts[i] = elements[i];
}
elements = newElememts;
System.out.println("扩容了: "+"旧容量,"+oldCapacity+"新容量,"+newCapacity);
}
/**
* 构造1:不指定数组容量:默认容量是10
*/
public ArrayList() {
//调用有参构造
this(DEFAULT_CAPACITY);
}
/**
* 构造2:指定初始容量,如果参数小于默认容量时用默认容量
* @param capacity
*/
public ArrayList(int capacity) {
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[])new Object[capacity];
}
/**
* 清除所有元素
*/
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
/**
* 元素的数量
* @return
*/
public int size() {
return size;
}
/**
* 是否为空
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 是否包含某个元素
* @param element
* @return
*/
public boolean contains(E element) {
return indexOf(element) != ELEMENT_NOT_FOUND;
}
/**
* 添加元素到尾部
* @param element
*/
public void add(E element) {
add(size, element);
}
/**
* 获取index位置的元素
* @param index
* @return
*/
public E get(int index) {
rangeCheck(index);
return elements[index];
}
/**
* 设置index位置的元素
* @param index
* @param element
* @return 原来的元素ֵ
*/
public E set(int index, E element) {
rangeCheck(index);
E old = elements[index];
elements[index] = element;
return old;
}
/**
* 在index位置插入一个元素:原来该位置的元素及后面的元素后移
* @param index
* @param element
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
//确保容量足够:查看容量是否足够,不够就扩容
ensureCapacity(size + 1);
for(int i = size; i > index; i--) {
elements[i] = elements[i-1];
}
elements[index] = element;
size++;
}
/**
* 删除index位置的元素:
* 具体操作是index后面的位置前移,覆盖掉要删除的元素。
* 所有数组的删除操作很麻烦。
* @param index
* @return 被删除的元素
*/
public E remove(int index) {
rangeCheck(index);
E old = elements[index];
//index后的元素前移
while (index < size-1) {
elements[index] = elements[index+1];
index++;
}
elements[--size] = null;
return old;
}
/**
* 删除指定元素
* @param element
* @return
*/
public int remove(E element) {
int index = indexOf(element);
remove(indexOf(element));
return index;
}
/**
* 查找元素的索引
* @param element
* @return
*/
public int indexOf(E element) {
if(element == null) {
for (int i=0; i < size; i++) {
if (elements[i] == null) return i;
}
}else {
//那么element一定不为null,放在前面调用equals绝对没问题
for (int i=0; i < size; i++) {
if (element.equals(elements[i])) return i;
}
}
return ELEMENT_NOT_FOUND;
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("[");
for (int i = 0; i < size; i++) {
if(i != 0)
string.append(", ");
string.append(elements[i]);
}
string.append("]");
return string.toString();
}
}