List接口
什么是接口?
在集合框架中,List是一个接口,继承自Collection。
站在数据结构的角度来看,List就是一个线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删改查以及变量等操作。
特点:
1、它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
2、它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
3、集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。
常见方法
方法 | 解释 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection<? extends E> c) | 尾插 c 中的元素 |
E remove(int index) | 删除 index 位置元素 |
boolean remove(Object o) | 删除遇到的第一个 o |
E get(int index) | 获取下标 index 位置元素 |
E set(int index, E element) | 将下标 index 位置元素设置为 element |
void clear() | 清空 |
boolean contains(Object o) | 判断 o 是否在线性表中 |
int indexOf(Object o) | 返回第一个 o 所在下标 |
int lastIndexOf(Object o) | 返回最后一个 o 的下标 |
List subList(int fromIndex, int toIndex) | 截取部分 list |
List的使用
注意:List是个接口,并不能直接用来实例化。
如果要使用,必须去实例化List的实现类。在集合框架中,ArrayList和LinkedList都实现了List接口。
例如
public class Test {
public static void main(String[] args) {
List<String> list = new LinkedList<>();
list.add("我");
list.add("爱");
list.add("学");
list.add("习");
//在索引为1的位置添加”不“
list.add(1,"不");
System.out.println(list);
System.out.println(list.contains("不"));
System.out.println(list.contains("嘻嘻"));
System.out.println(list.get(2));
String index = String.valueOf(list.indexOf("不"));
list.remove(index);
System.out.println(list);
//将索引为1的数替换为
list.set(1,"超级");
System.out.println(list);
}
细节:
当List接口保存的是整型,方法默认按照索引删除,若要按值删除,先调用indexOf方法获取该值对应的索引,再调用remove方法删除。
动态数组
动态数组 :对于用户来说,使用动态数组不需要关心数组长度,解决原生语言中数组定长的问是本质上,所谓的动态数组只是在基本数组的基础上封装到了类中,至于数组的长度由类的内部来进行扩容操作。
数组的扩容处理:
public void add(E element) {
this.elementDate[size] = element;
size++;
if (size == elementDate.length) {
//当前数组已满,需要扩容
grow();
}
}
private void grow() {
int oldLength = elementDate.length;
// 默认扩容为原来的一倍
int newLength = oldLength << 1;
// 将原数组中的所有内容拷贝到新数组中,超出的部分使用默认值来填充
Object[] newArray = Arrays.copyOf(elementDate,newLength);
elementDate= newArray;
}
public class MyArrayTest {
public static void main(String[] args) {
Seqlist<Integer> list = new MyArray<>(4);
list.add(1);
list.add(3);
list.add(5);
list.add(7);
list.add(9);
System.out.println(list);
}
}
在Debug模式下进行调试观察数组在存入7的时候,长度变为原来的两倍。
增删查改
public class MyArray<E> implements Seqlist<E> {
//保存当前线性表中真正保存的有效元素个数
private Object[] elementDate;
private int size;
public MyArray(){
this(10);
}
//从外部传入一个数组长度进行初始化操作
public MyArray(int size){
this.elementDate = new Object[size];
}
//默认尾插
@Override
public void add(E element) {
this.elementDate[size] = element;
size++;
if (size == elementDate.length) {
//当前数组已满,需要扩容
grow();
}
}
//对内部的数组进行扩容处理
private void grow() {
int oldLength = elementDate.length;
// 默认扩容为原来的一倍
int newLength = oldLength << 1;
// 将原数组中的所有内容拷贝到新数组中,超出的部分使用默认值来填充
Object[] newArray = Arrays.copyOf(elementDate,newLength);
elementDate= newArray;
}
//从中间插入元素
@Override
public void add(int index, E element) {
//判断用户传入的index是否合法
if(index < 0 || index >size){
throw new IllegalArgumentException("add index illegal!");
}
//index == size
if(index == size) {
add(element);
return;
}
//0 <=index <size
for (int i = size - 1; i>= index; i--) {
elementDate[i + 1] = elementDate[i];
}
//此时index位置待插入
elementDate[index] = element;
size++;
if(size == elementDate.length){
grow();
}
}
//删除指定索引位置的数
@Override
public E removeByIndex(int index) {
// 1.index合法性校验
if(!rangeCheck(index)) {
throw new IllegalArgumentException("remove index illegal!");
}
E oldVal = (E) elementDate[index];
// 从待删除元素开始,将当前位置的值覆盖为下一个元素的值
// 由于删除场景下索引不能取到size
// 因此要保证代码中所有位置均不越界,i + 1 < size
for (int i = index; i < size - 1; i++) {
elementDate[i] = elementDate[i + 1];
}
// 维护一下size属性,最后多的那个元素不影响我们的使用~下次添加元素时就会自动覆盖掉
size --;
return oldVal;
}
//删除数组中某个元素
@Override
public void removeByValue(E element) {
// 从前向后遍历,碰到第一个值为待删除的元素时,删除该元素并返回 ~
for (int i = 0; i < size; i++) {
if (elementDate[i].equals(element)) {
// 碰到第一个待删除的元素
removeByIndex(i);
return;
}
}
System.out.println("没有待删除的元素!");
}
//删除数组中值为element的所有元素
@Override
public void removeAllValue(E element) {
// 从前向后遍历,碰到索引对应的元素为待删除的元素,删除即可
for (int i = 0; i < size;) {
if (elementDate[i].equals(element)) {
// 当i指向的元素是待删除的元素,不能移动i
removeByIndex(i);
}else {
// 此时i指向的不是待删除元素,此时移动i的指向,继续判断下一个元素是否是待删除的元素
i ++;
}
}
}
//修改索引为index的元素为element
@Override
public E set(int index, E element) {
if(!rangeCheck(index)){
throw new IllegalArgumentException("set index illegal!");
}
E oldVal = (E) elementDate[index];
elementDate[index] = element;
return oldVal;
}
//查找数组中某个元素
@Override
public E get(int index) {
if(!rangeCheck(index)){
throw new IllegalArgumentException("get index illegal!");
}
return (E) elementDate[index];
}
private boolean rangeCheck(int index) {
if(index < 0 || index >= size){
return false;
}
return true;
}
@Override
public boolean contains(E element) {
for (int i = 0; i < size; i++) {
if (elementDate[i].equals(element)) {
return true;
}
}
return false;
}
@Override
public int indexOf(E element) {
for (int i = 0; i < size; i++) {
if (elementDate.equals(element)) {
return i;
}
}
throw new NoSuchElementException("indexOf error!" +
"Don't has this element");
}
public String toString(){
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < size; i++) {
sb.append(elementDate[i]);
if( i != size - 1){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
总结:
①动态数组在中间位置进行插入和删除,操作比较耗时。因为此时需要来回搬移元素。
②动态数组由于底层仍然使用的是数组,因此按索引查询值是比较快的。立即可以取得值。
千万不要武断的说,动态数组的插入一定就慢,若调用默认的add方法,尾插,也是很快的,不需要元素搬移~
除了增删查之外,动态数组一般需要连续的空间,当数据量较大时,有很大一部分空间的浪费~
假设此时数组长度为100,元素存满了,要保存下一个元素,就需要进行扩容,扩容之后数组长度变为了200
为了多存一个元素,浪费了99个空间
有没有一种数据结构,既能不关心元素的长度,又能比较快速的进行插入和删除,也不太浪费空间,由此我们开始进入链表的学习。