一、线性结构
概念:
线性结构是一个有序数据元素的集合;
特点:
1、第一个元素称为首元素,第二个元素称为尾元素
2、除首元素和尾元素外的其他元素都有一个直接前驱和直接后继。
3、元素存在一对一的线性关系
常见的线性结构有:
顺序表、链表、栈、队列
-----------------------ok,先介绍顺序表------------------------------------
二、顺序表
2.1、介绍
顺序表在计算机内存中以数组的形式保存的一种数据结构,它是用一组地址连续的存储单元依次存储线性表中的个个元素。
优点:查询速度快,因为可以根据下标直接添加
缺点:删除和添加速度慢,如果在数组尾部添加元素没有什么影响,如果在数组中间或者头部添加元素需要挪动大量元素,删除同理。
后续看代码和图片就理解啦
画的不好
强调重点:
一、地址连续
在内存中及物理地址必须是连续的(要知道物理地址和逻辑地址的区别)
二、依次存储
比如图中下标为1和3之间就不能空一个元素,下标为2的位置也必须有值,这就叫依次存储。
2.2、 基本功能的实现:
1、获取顺序表元素个数
2、在表尾添加元素
3、在指定位置添加元素
4、删除指定位置的元素
5、自动扩容(对ArraysList源码的扩容有帮助)
2.3和2.4节重点介绍一下指定位置添加元素和删除元素
2.5节介绍自动扩容
下面是代码实现:
/**
* 顺序存储结构
* @author xxp
*/
public class MyArrayList<T> {
/**
* 使用数组模拟顺序存储结构
*/
private Object[] items;
/**
* 存储元素的数量
*/
private int size;
/**
* 创建MyArrayList的时候需要指定大小
* @param capacity
*/
public MyArrayList(int capacity) {
//根据用户设定的大小来建立一个顺序表
this.items=new Object[capacity];
//初始化顺序表
this.size=0;
}
//-----------顺序存储结构的主要操作方法-----------------------------------
/**
* 扩容方法
* @param size
*/
public void reSize(int size){
//new一个新数组
Object[] newItems=new Object[size];
//把老数组中的值转移到新数组中
for (int i = 0; i < this.size; i++) {
newItems[i]=this.items[i];
}
//把新数组赋值给老数组
this.items=newItems;
}
/**
* 获取当前存储元素的数量
* @return
*/
public int size(){
return this.size;
}
/**
* 获取指定位置的元素
* @param i
* @return
*/
public T get(int i){
return (T) this.items[i];
}
/**
* 在尾部添加元素
*/
public void add(T t){
//判断是否数组已满
if (this.size==this.items.length){
//扩容到原来的2倍
reSize(this.items.length*2);
}
//在尾部添加不需要移动元素,直接根据下表添加就行
items[this.size]=t;
//添加完后让当前元素大小加1
size++;
}
/**
* 在指定位置添加元素
* @param position 添加位置
* @param t 添加的元素
*/
public void add(int position,T t){
//在指定位置添加元素,必须让元素从添加位置开始每个元素往后移动一个单位
//元素必须从最后一个元素开始移动,如果从添加的位置移动会导致前面的元素覆盖后面的元素,导致数据丢失
//size-1的意思:数组下标是从0开始的,size表是的是元素个数
for (int j = this.size-1; j >= position; j--) {
this.items[j+1]=this.items[j];
}
//全部移动完毕指定位置空出来之后,添加元素
this.items[position]=t;
//当前元素数量加1
size++;
}
/**
* 删除指定位置的元素
* @param position 位置
*/
public void remove(int position){
//删除元素跟添加元素思路一致
//指定位置的元素直接让后面元素依次往前移动,注意:不能从最后一个元素移动会导致前面元素被覆盖,导致数据丢失
for (int i = position+1; i <= this.size; i++) {
this.items[i-1]=this.items[i];
}
//当前元素数量减一
size--;
}
}
2.3、在指定位置添加元素
public void add(int position,T t){
for (int j = this.size-1; j >= position; j--) {
this.items[j+1]=this.items[j];
}
//全部移动完毕指定位置空出来之后,添加元素
this.items[position]=t;
//当前元素数量加1
size++;
}
时间复杂度是O(n)
在指定位置添加元素,让元素从添加位置开始每个元素往后移动一个单位
注意:元素必须从最后一个元素开始依次往后移动,如果从添加的位置移动会导致前面的元素覆盖后面的元素,导致数据丢失
size-1的意思:数组下标是从0开始的,size表是的是元素个数
2.4、删除指定位置的元素
public void remove(int position){
for (int i = position+1; i <= this.size; i++) {
this.items[i-1]=this.items[i];
}
//当前元素数量减一
size--;
}
时间复杂度是O(n)
删除元素跟添加元素思路一致
从指定位置的元素开始,让后面元素依次往前移动。
注意:不能从最后一个元素依次移动会导致前面元素被覆盖,导致数据丢失
2.5、自动扩容
/**
* 扩容方法
* @param size
*/
public void reSize(int size){
//new一个新数组
Object[] newItems=new Object[size];
//把老数组中的值转移到新数组中
for (int i = 0; i < this.size; i++) {
newItems[i]=this.items[i];
}
//把新数组赋值给老数组
this.items=newItems;
}
/**
* 在尾部添加元素
*/
public void add(T t){
//判断是否数组已满
if (this.size==this.items.length){
//扩容到原来的2倍
reSize(this.items.length*2);
}
//在尾部添加不需要移动元素,直接根据下表添加就行
items[this.size]=t;
//添加完后让当前元素大小加1
size++;
}
在添加元素的时候可能会出现数组越界异常,这时候就要进行扩容。
扩容的思路也非常简单:
定义一个新数组,长度为原来的两倍,然后通过遍历把老数组中的元素放到新数组中,最后把新数组赋值给老数组就实现啦扩容。
之后可以在添加操作处加上if判断,判断当前添加元素后是否超过当前数组的长度。
2.6、ArraysList扩容源码
这里我截取一段扩容的源码
可以看到
int newCapacity = oldCapacity + (oldCapacity >> 1);
这句代码就是将老的数组长度通过位运算的方式扩大为原来的1.5倍
elementData = Arrays.copyOf(elementData, newCapacity);
这句代码就是将新数组的数据赋值给老数组,它采用的是Arrays自带的copyOf方法,这个方法最终是采用内存拷贝的方式,这个方式比我们使用for循环赋值要快的多的多
整体思路跟我们的差不多
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩大为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}