Iterable也是一个接口,表示实现该接口的类是可以逐个元素进行遍历的。
Collection也是一个接口,该接口中规范了后序容器中常用的一些方法。
在集合框架中,List是一个接口,继承自Collection。
站在数据结构的角度来看,List就是一个线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删改查以及变量等操作。
List中提供的方法:
注意:List是个接口,并不能直接用来实例化。
如果要使用,必须去实例化List的实现类。在集合框架中,ArrayList和LinkedList都实现了List接口。
关于它们的具体使用,请细读以下文章。
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
在《大话数据结构》中这么说的:若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。
如下图:
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
而在顺序表中可以分成:
静态顺序表:使用定长数组存储。
动态顺序表:使用动态开辟的数组存储。
实际上我们可以把顺序表当作一个数组,如下是一个顺序表的示例代码:
public class MyArrayList {
private int[] elem; //用来存放数据
private int userSize;//代表当前顺序表中的有效数据个数
private static final int DEFAULT_SIZE = 10;
//定义静态常量,默认MyArrayList的容量是10,final-不能改
public MyArrayList(){
this.elem = new int [DEFAULT_SIZE];
}
/**
* 指定容量
* @param initCapacity
* 我们可以通过修改initCapacity来指定其容量大小,可以根据自己想要的大小去重载构造器
*/
public MyArrayList(int initCapacity){
this.elem = new int [initCapacity];
}
}
那么既然顺序表可以看作是数组,那么对顺序表的打印其实也类似于遍历数组。
// 打印顺序表,注意:该方法并不是顺序表中的方法,为了方便看测试结果给出的
/**
* 实际上就是遍历数组
*/
public void display() {
for (int i = 0; i < this.userSize; i++) {
System.out.print(this.elem[i]+" ");
}
}
数据结构作为一门学科,我们必须明确一个点就是数据结构逻辑是非常严谨的。下面我们开始设计增删查改的功能。
// 获取顺序表长度
public int size() {
return this.userSize;
}
因为userSize
代表当前顺序表中的有效数据个数,是数组也就是顺序表的长度。
在这之前,我们必须注意一个点:
代码如下:
//判断顺序表是否满了
public boolean isFull(){
if(this.userSize == this.elem.length){
return true;
}
return false;
}
//扩容操作
if (isFull()){
//扩容
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
关于扩容,其实在Java中ArrayList有相关的扩容机制,下文会有所介绍。
总代码如下:
public void add(int data) {
if (isFull()){
//扩容
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
this.elem[this.userSize] = data;
this.userSize++;
}
public boolean isFull(){
if(this.userSize == this.elem.length){
return true;
}
return false;
}
同样,在此之前我们需要考虑以下几点:
判断完成之后就是在添加pos位置元素后,将整个数组从指定位置一个一个向后移动以一个位置。
//判断顺序表是否满了
public boolean isFull(){
if(this.userSize == this.elem.length){
return true;
}
return false;
}
//扩容操作
if (isFull()){
//扩容
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
//检查pos位置是否合法
//这里的private,也可以是public,这里主要体现了封装性
private void checkPos(int pos) {
if(pos < 0 || pos > usedSize) {
throw new IndexOutOfException
(pos + "pos位置不合法");
}
}
总代码如下:
// 在 pos 位置新增元素
//add方法重载
public void add(int pos, int data) {
//对pos的检查
checkPos(pos);
if (isFull()){
//扩容
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
//将整个数组从指定位置一个一个向后移动以一个位置
for (int i = this.userSize - 1; i >= pos ; i--) {
this.elem[i+1] = this.elem[i];
}
this.elem[pos] = data;
this.userSize++;
}
// 判定是否包含某个元素
public boolean contains(int toFind) {
for (int i = 0; i < this.userSize; i++) {
if (this.elem[i] == toFind){
return true;
}
}
/**
* 如果这里找的是一个引用类型的话,this.elem[i] == toFind就不能用等号了
* equals -> 返回值是true false
* compare to -> 整型 -> 比较大于 小于 等于
*/
return false;
}
// 查找某个元素对应的位置
public int indexOf(int toFind) {
for (int i = 0; i < this.userSize; i++) {
if (this.elem[i] == toFind) {
return i;
}
}
return -1;
}
同样,在此之前我们需要判断pos位置的合法性
//检查pos位置是否合法
private void checkPos(int pos) {
if(pos < 0 || pos > usedSize) {
throw new IndexOutOfException
(pos + "pos位置不合法");
}
}
public class PosOutOfBoundException extends RuntimeException{
public PosOutOfBoundException() {
}
public PosOutOfBoundException(String message) {
super(message);
}
}
总代码:
// 获取 pos 位置的元素
public int get(int pos) {
checkPos(pos);
return this.elem[pos];
}
// 给 pos 位置的元素设为 value(理解为更新)
public void set(int pos, int value) {
checkPos(pos);
this.elem[pos] = value;
}
首先判断顺序表是否为空值,其次找到toRemove
的下标,这里我们用indexOf
方法就可以找到,indexOf
方法返回值为-1
,则打印没有这个数据!否则从index
开始,将后一个元素的值赋值给前一个元素,并且usedSize--
。
这里的index
方法即是:
// 查找某个元素对应的位置
public int indexOf(int toFind) {
for (int i = 0; i < this.userSize; i++) {
if (this.elem[i] == toFind) {
return i;
}
}
return -1;
}
//删除第一次出现的关键字key
public void remove(int toRemove) {
//先找有没有这个数
int index = indexOf(toRemove);
if (index == -1){
System.out.println("没有这个数据!");
return;
}
for (int i = index; i < this.userSize - 1; i++) {
this.elem[i] = this.elem[i + 1];
}
/*
引用类型
this.elem[useSize - 1] = null;
*/
this.userSize--;
}
// 清空顺序表
public void clear() {
/*
如果elem是引用类型
for (int i = 0; i < this.userSize; i++) {
this.elem[i] = null;
}
*///内存泄露解决
this.userSize = 0;
}
实际上,以上讲的东西偏底层,在我们使用Java开发的时候,手撸以上代码是很少的,因为在Java中有ArrayList这个类,且ArrayList是具备以上的功能的。下面就让我们来介绍一下ArrayLIst吧。
ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。ArrayList 继承了 AbstractList ,并实现了 List 接口。
底层使用数组:
private transient Object[] elementData;
【说明】
ArrayList
是以泛型方式实现的,使用时必须要先实例化
ArrayList
实现了RandomAccess
接口,表明ArrayList
支持随机访问
ArrayList
实现了Cloneable
接口,表明ArrayList
是可以clone
的
ArrayLists
实现了Serializable
接口,表明ArrayList
是支持序列化的
和Vector
不同,ArrayList
不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector
或者CopyOnWriteArrayList
ArrayList
底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
ArrayList
提供了三个构造函数:
ArrayList()
:默认构造函数,提供初始容量为10的空列表。
ArrayList(int initialCapacity)
:构造一个具有指定初始容量的空列表。
ArrayList(Collection extends E> c)
:构造一个包含指定 collection
的元素的列表,这些元素是按照该 collection
的迭代器返回它们的顺序排列的。
public static void main(String[] args) {
// ArrayList创建,推荐写法
// 构造一个空的列表
List<Integer> list1 = new ArrayList<>();
// 构造一个具有10个容量的列表
List<Integer> list2 = new ArrayList<>(10);
list2.add(1);
list2.add(2);
list2.add(3);
// list2.add("hello"); // 编译失败,List已经限定了,list2中只能存储整形元素
// list3构造好之后,与list中的元素一致
ArrayList<Integer> list3 = new ArrayList<>(list2);
// 避免省略类型,否则:任意类型的元素都可以存放,使用时将是一场灾难
List list4 = new ArrayList();
list4.add("111");
list4.add(100);
}
ArrayList常见操作
public static void main(String[] args) {
ArrayList<Integer>list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
list2.add(3);
list2.add(4);
list2.add(5);
//第一种遍历 for循环+下标
for (int i = 0; i < list2.size(); i++) {
System.out.print(list2.get(i)+" ");
}
System.out.println();
//第二种遍历 for-each
for (Integer x :list2){
System.out.print(x+" ");
}
System.out.println();
//第三种遍历 迭代器
Iterator<Integer> it = list2.iterator();
while (it.hasNext()){
System.out.print(it.next()+" ");
}
System.out.println();
System.out.println("=====================");
ListIterator<Integer>listIterator = list2.listIterator();
while(listIterator.hasNext()){
System.out.print(listIterator.next()+" ");
}
System.out.println();
System.out.println("=====================");
//升华版本
ListIterator<Integer>listIterator2 = list2.listIterator(list2.size());
while(listIterator2.hasPrevious()){
System.out.print(listIterator2.previous()+" ");
}
System.out.println();
//5 4 3 2 1
第一种遍历:
1 2 3 4 5
第二种遍历:
1 2 3 4 5
第三种遍历:
1 2 3 4 5
=====================
第三种遍历:
1 2 3 4 5
=====================
第三种遍历升华:
5 4 3 2 1
自动扩容
/**
* 笔试题:cvte
* str1:welcome to cvte
* str2:come
* 描述:删除第一个字符串当中出现的所有的第二个字符串的字符!
* 结果:wtl t vt
* 要求用ArrayList完成!!!!
* @param str1
* @param str2
* @return
*/
public static List<Character> func(String str1,String str2){
//1.遍历str1这个字符串 看看当中是不是存在str2中的字符
List<Character>list = new ArrayList<>();
for (int i = 0; i < str1.length(); i++) {
char ch = str1.charAt(i);
if(!str2.contains(ch+"")){
list.add(ch);
}
}
return list;
}
public static void main(String[] args) {
String str1 = "welcome to ctve";
String str2 = "come";
List<Character>ret = func(str1,str2);
for (char ch : ret){
System.out.print(ch);
}
}
至此,顺序表的讲解就告一段落。我们可以总结出,顺序表适合用来处理静态数据,对其进行查找和更新,但是却不适合用来插入和删除数据,因为在插入数据的时候,需要移动后面的所有元素,这样子时间复杂度der一下就上来了,同时其删除效率也低,在扩容的时候也容易出现浪费空间的情况。
那么如何解决这些问题呢?接下来我们将会详细介绍链表。