目录
1. 关于ArrayList和LinkedList(顺序表和链表)
1. 关于ArrayList和LinkedList(顺序表和链表)
ArrayList | LinkedList | |
别名 | 线性顺序表 | 线性链表 |
所属包 | java.util | |
支持数据类型 | 引用类型 | |
数据存储结构 | 顺序存储 | 链式存储 |
优点 | 便于直接查找和修改 | 便于新增和删除元素 |
缺点 | 增删元素可能需要搬动大量的其余元素 | 查找元素可能需要扫描较多的其余元素 |
解释:
顺序存储:ArrayList是用数组来存放数据的,故逻辑上相邻的元素,存储位置上必定相邻。元素的索引正是数组的下标。这也就决定了ArrayList可以随意且直接地读取其中任意一个元素,并可以修改,但增删元素在最糟糕情况之下,也就是在首位新增元素或删除元素时,后面所有元素都要搬家。
链式存储:LinkedList是用一个个结点对象来存放数据的,类库中的LinkedList是双向链表,故结点里面包含前驱指针(存放前一结点的内存地址)、元素值、后继指针(存放后一结点的内存地址)。故增加或删除元素时只需要修改指针指向的结点即可。但问题就出在必须通过头/尾结点“顺藤摸瓜”才能看到要找的元素。
2. 自定义单向链表
整体设计思路:
1. 链表元素都是由结点构成的,这种结点有两个用处:
1. 存数据
2. 存逻辑上相邻结点的指针
本次要做一个单向链表,结点只需要两个属性:
1. 当前元素值
2. 后驱指针
public class Node {
Object data;//由于元素是引用类型,所以不管是什么类型都是Object的子类型
Node next;
public Node() {}
public Node(Object elem) {//本次为了凸显结点指针移动的过程,没有写全参构造
data = elem;
}
}
2. 创建链表类继承List接口,属性中加入一个头结点(引导访问指针)和一个计数器(统计元素个数)
3. 运用所学知识尝试实现contains()、add()、get()等简单的方法
其中,add()、remove()是本次学习链表的重头戏,相信学过C语言版数据结构的同学们好多都被指针的移动给转懵了(包括本Reno),借此机会好好梳理下如何移动元素指针把新结点穿起来
package com.reno.d0811;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public class LinkList implements List {
Node head = new Node();//头结点可以引导我们开始访问数组元素,所以不用存数据
private int size;//计数器,仅可通过函数读取链表长度
//返回计数器的值
@Override
public int size() {
return size;
}
//判断链表是不是空的
@Override
public boolean isEmpty() {
return size == 0;
}
//判断链表中是否有与参数相匹配的元素
@Override
public boolean contains(Object o) {
Node p = head;
for (int i = 0; i < size; i++) {
p = p.next;
if (o.equals(p.data)) {
return true;
}
}
return false;
}
//此函数用于在链表尾部位置新增结点,属于插入结点的特殊情形
@Override
public boolean add(Object o) {
add(size, o);
return true;
}
//删除与参数列表匹配的第一个结点
@Override
public boolean remove(Object o) {
if (!this.contains(o)) {//如果不包含与参数匹配的目标元素,就不用执行删除了
return false;
}
Node p = head;
//查找元素是否与参数中的值匹配
for (int i = 0; i < size && !o.equals(p.next.data); i++) {
p = p.next;//后移指针直到找到目标元素的前一位
}
p.next = p.next.next;//将待删除位置后一位的地址作为待删除元素前一位的后驱,以后访问时就可以跳过待删除元素了
size--;//更新元素个数
return true;
}
//查找指定元素的位置
@Override
public Object get(int index) {
Node p = head;//创建一个指针指向头结点
for (int i = 0; i <= index; i++) {
p = p.next;//依次向后移动指针直到目标位置
}
return p.data;//指针刚好移到目标位置,直接返回元素的值
}
@Override
public void add(int index, Object element) {
Node p = head;//定义一个Node型指针默认指向头结点
//找到上一个结点
for (int i = 0; i < index; i++) {
p = p.next;//移动指针
}
//1. 新建结点对象
//2. 把元素存到结点data部分
Node n = new Node(element);
//3. 将后面结点的地址作为新结点的后驱指针,也就是把新结点和后面结点连起来
n.next = p.next;
//4. 当前结点地址作为前一结点的后驱指针,也就是把新结点和前面结点连起来
p.next = n;
size++;
}
/*实现List接口里面的其余函数就先不展示了*/
}
4. 创建测试类,测试下自己定义的链表
package com.reno.d0811;
public class Runner {
public static void main(String[] args) {
LinkList l = new LinkList();
l.add("Bob");
l.add("Mary");
l.add("Mike");
l.add("Rich");
l.add(0, "Reno");
for (int i = 0; i < l.size(); i++) {
System.out.println(l.get(i));
}
System.out.printf("当前链表长度%d\n", l.size());
System.out.printf("-------------------------------------------------------\n");
System.out.println(l.remove("Mike"));
System.out.printf("-------------------------------------------------------\n");
for (int i = 0; i < l.size(); i++) {
System.out.println(l.get(i));
}
System.out.printf("当前链表长度%d\n", l.size());
System.out.printf("-------------------------------------------------------\n");
System.out.println(l.contains("Bob"));
System.out.println(l.contains("Mike"));
}
}
预期输出:
3. 自定义链表Q&A
Q:在链表中插入或删除结点时,扫描链表的指针p为何要停在目标位置前面一位,而不正好是在目标位置?
A:对于单向链表,插入和删除结点时一定要修改目标位置前一位的指针
Q:在链表中插入元素时,为何要先把新结点和后面部分连起来?先让新结点和前面部分连起来可以吗?
A:对于单向链表,表示用来指向后续结点的指针是必不可少的,如果先改了目标前面一位的next指针,就没有可以表示用来指向后面结点的指针了,链表就会断掉。在以往的学习中特别强调对象一定要有指向它的指针,否则对象就成为垃圾从而被回收