数据结构
数据结构--用于储存和管理数据的方式
就是以下几种方式:
神奇的网站
这个网站可以完成对各种数据结构的演示 推荐大家去使用 网址如下:
数据结构和算法动态可视化 (Chinese) - VisuAlgo
线性表之动态数组
线性表
线性表:具有n个相同类型元素的有限序列
几个概念
索引0处的节点对于索引1处的节点来说称之为前驱节点
索引1处的节点对于索引0处的节点来说称之为后继节点
索引0处的节点称之为首节点 索引n-1处的节点称之为尾节点
常见的线性表:数组、链表、栈、队列、哈希表
数组
数组:即顺序储存的线性表 就是元素的内存地址是连续的
假使我有一个这样一个数组:int[] array = new int[]{11, 22, 33};
数组的弊端
许多编程语言都无法进行数组容量的动态改变 但是我们肯定是希望能够按需改变我们的数组容量 因此我们可以设置这样一个接口 那就是ensureCapacity()方法
数组的接口设计
int size():记录数组元素个数
boolean isEmpty():查看数组是否为空
boolean contains(E element):查看数组是否包含指定元素
void add(E element):将指定元素添加到数组最后
E get(int index):获取指定位置处的元素
E set(int index, E element):修改指定位置处的元素 并返回修改前指定位置处的元素
void add(int index, E element):往指定位置处添加指定元素
E remove(int index):删除指定位置处的元素
int indexOf(E element):查看指定元素的位置
void clear():清空数组
数组的代码实现
public class ArrayList{
// 定义两个私有成员变量
private int size;
private int[] elements;
// 定义两个常量
private static final int DEFAULT_CAPACITY = 10;
private static final int ELEMENT_NOT_FOUND = -1;
// 定义一个带参构造方法
public ArrayList(int capacity){
// 为数组容量重新赋值
capacity = (capacity <= DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
// 然后为数组赋初始值
elements = new int[capacity];
}
// 定义一个无参构造方法
public ArrayList(){
// 直接调用带参构造方法即可
this(DEFAULT_CAPACITY);
}
// 获取数组元素个数
public int size(){
return size;
}
// 判断数组是否为空
public boolean isEmpty(){
return size == 0;
}
// 判断数组是否包含指定元素
public boolean contains(int element){
return indexOf(element) != -1;
}
// 将指定元素添加到数组末尾处
public void add(int element){
// 直接调用add(int index, int element)方法
add(size, element);
}
// 将指定元素添加到指定位置处
public void add(int index, int element){
// 首先进行边界检查
rangeCheckForAdd(index);
// 然后将指定索引后面的元素均后退一步 但是是倒叙遍历
for(int i = size - 1; i >= index; i--){
elements[i + 1] = elements[i];
}
// 然后将指定元素插入到指定位置处
elements[index] = element;
// 然后最后导致size++
size++;
}
// 获取指定索引处的元素
public int get(int index){
// 首先进行边界检查
rangeCheck(index);
// 直接返回指定索引处的元素
return elements[index];
}
// 重置指定位置处的元素
public int set(int index, int element){
// 首先进行边界检查
rangeCheck(index);
// 首先获取指定位置处上的元素
int oldElement = elements[index];
// 重置指定位置处上的元素
element[index] = element;
// 返回指定位置处的旧值
return oldElement;
}
// 删除指定位置处的元素 并且返回
public int remove(int index){
// 首先进行边界检查
rangeCheck(index);
// 定义一个变量 用于记录指定位置处上的元素
int oldElement = elements[index];
// 然后遍历数组 将指定位置后面的元素向前进一步
for(int i = index; i < size - 1; i++){
elements[i] = elements[i + 1];
}
// 最后导致size--
size--;
// 然后返回指定位置处上的旧值
return oldElement;
}
// 获取指定元素在数组中的索引 如果没有 就返回一个常量
public int indexOf(int element){
// 遍历数组
for(int i = 0; i < size; i++){
if(elements[i] == element)return i;
}
// 如果在数组中找不到的话 那么就直接返回常量
return ELEMENT_NOT_FOUND;
}
// 清空数组
public void clear(){
// 数组的清空其实上是逻辑清空 实际上并没有真正清空 而是让你访问不到而已
size = 0;
}
// 正常的边界检查
public void rangeCheck(int index){
if(index < 0 || index >= size){
outOfBounds(index);
}
}
// 针对add()方法的边界检查
public void rangeCheckForAdd(int index){
if(index < 0 || index > size){
outOfBounds(index);
}
}
// 对索引越界做出相关处理
public void outOfBounds(int index){
throw new IndexOutBoundsException("Size:" + size + " Index:" + index);
}
// 重写toString方法
@override
public String toString(){
// 创建一个StringBuilder对象
StringBuilder sb = new StringBuilder();
// 添加前缀
sb.append("Size:").append(size).append(" [");
// 遍历数组 将内中元素拼接到sb后面
for(int i = 0; i < size; i++){
if(i != 0){
sb.append(", ");
}
sb.append(elements[i]);
}
// 添加后缀
sb.append("]");
// 返回最终结果
return sb.toString();
}
}
添加泛型以后的代码实现
public class ArrayList<E>{
// 定义两个私有成员变量
private int size;
private E[] elements;
// 定义两个常量
private static final DEFAULT_CAPACITY = 10;
private static final ELEMENT_NOT_FOUND = -1;
// 定义两个构造方法
public ArrayList(int capacity){
// 为数组容量重新赋值
capacity = (capacity <= DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[])new Object[capacity];
}
public ArrayList(){
// 直接调用带参构造方法
this(DEFAULT_CAPACITY);
}
// 获取数组元素个数
public int size(){
return size;
}
// 判断数组是否为空
public boolean isEmpty(){
return size == 0;
}
// 判断数组是否含有指定元素
public boolean contains(E element){
return indexOf(element) != -1;
}
// 将指定元素添加到数组末尾处
public void add(E element){
// 直接调用add(int index, E element)方法
add(size, 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++
size++;
}
// 删除指定位置处的元素
public E remove(int index){
// 首先对指定索引进行边界检查
rangeCheck(index);
// 然后定义一个变量 用于储存指定索引处的元素
E oldElement = elements[index];
// 然后遍历指定索引后面的元素 并将他们均往前走一步 记的是正序遍历
for(int i = index + 1; i < size; i++){
elements[i - 1] = elements[i];
}
// 然后在size--的同时顺便完成最后索引处元素的置空
elements[--size] = null;
// 返回指定索引处的旧值
return oldElement;
}
// 获取指定索引处的元素
public E get(int index){
// 首先对指定索引进行边界检查
rangeCheck(index);
// 返回指定索引处的元素
return elements[index];
}
// 重置指定索引处的元素
public E set(int index, E element){
// 首先对指定索引进行边界检查
rangeCheck(index);
// 然后定义一个变量 用于记录指定索引处的旧值
E oldElement = elements[index];
// 然后重置指定索引处的值
elements[index] = element;
// 返回最终结果
return oldElement;
}
// 获取指定索引在数组中的索引
public int indexOf(E element){
// 有可能出现参数为空的现象 所以需要进行条件判断
if(element == null){
// 遍历数组 然后看有无对应的元素
for(int i = 0; i < size; i++){
if(elements[i] == null)return i;
}
}else{
// 遍历数组
for(int i = 0; i < size; i++){
if(elements[i].equals(element))return i;
}
}
// 如果数组中没有对应的值的话 那么就直接返回常量
return ELEMENT_NOT_FOUND;
}
// 清空数组
public void clear(){
// 我们这次需要做的是将每一个数组元素的引用给消除掉 从而销毁每一个对象所占有的内存空间
for(int i = 0; i < size; i++){
elements[i] = null;
}
// 最终必然会导致size=0
size = 0;
}
// 正常的边界检查
public void rangeCheck(int index){
if(index < 0 || index >= size){
outOfBounds(index);
}
}
// 针对add()方法的边界检查
public void rangeCheckForAdd(int index){
if(index < 0 || index > size){
outOfBounds(index);
}
}
// 对索引越界进行处理
public void outOfBounds(int index){
throw new IndexOutOfBoundsException("Size:" + size + " Index:" + index);
}
// 重写toString方法
public String toString(){
// 创建一个StringBuilder对象
StringBuilder sb = new StringBuilder();
// 添加前缀
sb.append("Size:").append(size).append(" [");
// 遍历数组 将每个元素都拼接到sb后面去
for(int i = 0; i < size; i++){
if(i != 0){
sb.append(", ");
}
sb.append(elements[i]);
}
// 添加后缀
sb.append("]");
// 返回最终结果
return sb.toString();
}
// 数组扩容操作
public void ensureCapacity(int size){
// 首先获取以下旧数组的容量
int oldCapacity = elements.length;
// 如果参数没有超过旧数组的容量的话 那么就不做处理
if(oldCapacity >= size)return;
// 重新定义数组容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 然后创建一个新数组
E[] newElements = (E[])new Object[newCapacity];
// 然后将旧数组的元素直接拷贝到新数组中
for(int i = 0; i < size; i++){
newElements[i] = elements[i];
}
// 然后将旧数组指向新数组
elements = newElements;
// 最后进行以下提示
System.out.println(oldCapacity + "扩容为" + newCapacity);
}
}
添加泛型后的代码实现的一些细节
空值的讨论
如果允许数组中储存空值的话 那么就可以在indexOf()中进行分类讨论
但是如果不允许数组中储存空值的话 那么可以在add()方法中把控输入并且不需要进行分类讨论 输入如下代码
public void add(int index, E element){
if(element == null)return;
……
}
对象数组详解
1.为什么这个对象数组储存的是地址而不是值呢?
假使我的代码如下所示
Object[] objects = new Object[10];
Object[0] = new people(11, 170);
你看上面这个例子:首先Object类元素占用4个字节的内存空间
而people类则含有两个成员变量 都是int类型 所以一个people类占用8个字节 试想一下一个4个字节的内存空间怎么能够做得到储存8个字节的数据 做不到 因此我们才采用数组存值、然后通过引用指向数据的方式
2.elements = null 和 elements[i] = null 的区别?
对于elements = null来说 他首先会让它所指向的对象数组销毁掉 然后再让对象数组所指向的对象占用的内存空间给销毁掉
而对于elements[i] = null 来说 他直接将它所指向的对象所占用的内存空间给销毁掉了
3.我们怎么知道对象有无死亡?
我们可以在销毁的对象内部重写finalize()方法 比如你是销毁的people类对象 那么你就是在people类内部重写finalize()方法
然后你可以在主方法中写一个System.gc()用于提醒jvm回收机制赶紧回收垃圾 因为jvm回收机制是会在内存快满的时候才开始进行清理工作 而我们的时间可是很宝贵的 哪里耗得起
动态数组的内存管理细节
这边我主要想解释以下第二个图
为什么要添加框出来的代码呢?这是因为遍历完毕后 size-1和size-2索引处的地址指向相同到时候如果想要删除的话 是不能完全删除掉的 因为数据被两个引用指向 你其实只删除了一个引用
查看动态数组的源代码
你自己写完以后 可以对照着java官方给出的版本进行对照 应该吻合度还蛮高的 按住ctrl键不放即可 然后点击java.util.ArrayList