链表
链表(Linked List)
动态数组有个明显得缺点
- 可能造成内存空间的大量浪费
能否用到多少就申请多少内存?
- 链表可以办到这一点
链表是一种链式存储的线性表, 所有元素的内存地址不一定是连续的
链表(Linked List)接口设计
链表的大部分接口和动态数组是一致的
int size();//元素的数量
boolean isEmpty();//是否为空
boolean contains(E element);//是否包含某个元素
void add(E element);//添加元素到最后面
E get(int index);//返回index位置对应的元素
E set(int index, E element);//设置index位置的元素
void add(int index, E element);//往index位置添加元素
E remove(int index);//删除index位置对应的元素
int indexOf(E element);//查看元素的位置
void clear();//清除所有元素
由于链表的大部分接口和动态数组一致,我们抽取出一个共同的 List
接口
/**
* list 接口供动态数组和链表实现
* @param <E> 泛型
*/
public interface List<E> {
// 表示元素不存在
static final int ELEMENT_NOT_FOUND = -1;
/**
* 清除所有元素
*/
void clear();
/**
* 元素的数量
* @return
*/
int size();
/**
* 是否为空
* @return
*/
boolean isEmpty();
/**
* 是否包含某个元素
* @param element
* @return
*/
boolean contains(E element);
/**
* 添加元素到尾部
* @param element
*/
void add(E element);
/**
* 获取index位置的元素
* @param index
* @return
*/
E get(int index);
/**
* 设置index位置的元素
* @param index
* @param element
* @return 原来的元素ֵ
*/
E set(int index, E element);
/**
* 在index位置插入一个元素
* @param index
* @param element
*/
void add(int index, E element);
/**
* 删除index位置的元素
* @param index
* @return
*/
E remove(int index);
/**
* 查看元素的索引
* @param element
* @return
*/
int indexOf(E element);
}
再将一些通用的字段与方法放到一个抽象类中,无论是动态数组还是链表都只需要继承这个抽象类
/**
* 动态数组和链表公共的方法 其他方法有各自的实现
* @param <E> 泛型
*/
public abstract class AbstractList<E> implements List<E>{
/**
* 元素数量
*/
protected int size;
/**
* 元素的数量
* @return
*/
public int size(){
return size;
}
/**
* 是否为空
* @return
*/
public boolean isEmpty(){
return size == 0;
}
/**
* 是否包含某个元素
* @param element 元素
* @return
*/
public boolean contains(E element){
return indexOf(element) != ELEMENT_NOT_FOUND;
}
/**
* 添加元素到最后面
* @param element
*/
public void add(E element){
add(size,element);
}
/**
* 越界异常提示
* @param index
*/
protected void outOfBounds(int index){
throw new IndexOutOfBoundsException("Index:"+index+", Size:"+size);
}
/**
* 边界判断
* @param index
*/
protected void rangeCheck(int index){
if (index < 0 || index >= size){
this.outOfBounds(index);
}
}
/**
* 添加时边界判断
* @param index
*/
protected void rangeCheckForAdd(int index){
if (index < 0 || index > size){
this.outOfBounds(index);
}
}
}
创建 LinkedList 继承 AbstractList
- size属性记录存储数据的数量,创建first属性引用链表的第0个元素,来管理链表的数据
- 创建私有类Node,其中的element属性用于存储元素,next属性用于指向链表中的下一个结点
public class LinkedList<E> extends AbstractList<E>{
/**
* 元素所在的结点
*/
private Node<E> first;
/**
* 内部类 结点
* @param <E>
*/
private static class Node<E>{
E element;//元素
Node<E> next;//下个元素的地址
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
}
默认构造方法
链表的创建与动态数组不同,动态数组在构造时需要传入一个空间属性,来决定这个数组的容量。但链表元素是在添加时才创建的,内存地址不一定是连续的。所以链表不需要在单独设计构造方法,使用默认构造方法
清空元素 - clear()
将first指向null,释放链表所有node,同时size置为0
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 清除所有元素
*/
public void clear(){
size = 0;
first = null;
}
......
}
查找结点 - node(int index)
此时需要添加node()方法获取索引(index)位置对应的结点
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 获取index位置对应的结点对象
* @param index
* @return
*/
private Node<E> node(int index){
rangeCheck(index);
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
......
}
添加元素 - add(int index, E element)
- 添加数据结点时,需要创建一个结点存储数据,并将该结点拼接到最后结点的后面,然后size加1
- 需要区分当前链表没有数据,新结点拼接到first和当前链表有数据,新结点拼接到最后的结点
- 添加元素尤其要注意 0 位置,给空链表添加first结点是个特殊情况
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 返回index位置对应的元素
* @param index
* @return
*/
public E get(int index){
return node(index).element;
}
/**
* 设置index位置的元素
* @param index
* @param element
* @return
*/
public E set(int index, E element){
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
/**
* 往index位置添加元素
* @param index
* @param element
*/
public void add(int index, E element){
//边界判断
rangeCheckForAdd(index);
if (index == 0){// 给空链表添加第一个元素的情况
//创建新结点,first结点指向这个新结点
first = new Node<>(element,first);
}else {
//index上一个结点
Node<E> prev = node(index - 1);
//创建新结点,prev结点指向这个新结点
prev.next = new Node<>(element, prev.next);
}
size++;
}
......
}
删除元素 - remove(int index)
- 首先找到删除结点的前一个结点(prev),然后通过变更(prev)结点next指针指向删除结点的下一个结点,然后size减1
- 需要判断是否删除的第0个元素,如果是,则使用first结点指向第1个结点
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 删除index位置对应的元素
* @param index
* @return
*/
public E remove(int index){
//边界判断
rangeCheck(index);
//记录结点
Node<E> node = first;
if (index == 0){ // 删除第一个元素是特殊情况
first = first.next;
}else {
//index上一个结点
Node<E> prev = node(index - 1);
//记录删除的结点
node = prev.next;
//node结点下一个结点,prev结点指向这个结点
prev.next = node.next;
}
size --;
return node.element;
}
......
}
查找元素位置 - indexOf(E element)
-
查找指定元素的索引,需要遍历所有结点,找到结点对应的元素与执行元素相等
如果需要支持结点element为null,则需要分情况处理
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 查看元素的位置
* @param element
* @return
*/
public int indexOf(E element){
// 取出头结点
Node<E> node = first;
if(element ==null){// 当element为null时的处理
// 遍历结点, 找到存储为null的结点, 返回索引
for (int i = 0; i < size; i++) {
if (node.element == null) return i;
node = node.next;
}
}else {
// 遍历结点, 找到存储的元素与指定元素相等的结点, 返回索引
for (int i = 0; i < size; i++) {
if (element.equals(node.element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
......
}
打印元素 - toString()
- 重写toString方法
- 在toString 方法中将元素拼接成字符串
- 字符串拼接建议使用StringBuilder
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 打印
* @return
*/
@Override
public String toString() {
//Size = ? , [1, 2, 3]
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0){
string.append(", ");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
......
}
完整代码 --- 单向链表
/**
* 单链表的实现
* @param <E> 泛型
*/
public class LinkedList<E> extends AbstractList<E>{
/**
* 元素所在的结点
*/
private Node<E> first;
/**
* 内部类 结点
* @param <E>
*/
private static class Node<E>{
E element;//元素
Node<E> next;//下个元素的地址
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
/**
* 返回index位置对应的元素
* @param index
* @return
*/
public E get(int index){
return node(index).element;
}
/**
* 设置index位置的元素
* @param index
* @param element
* @return
*/
public E set(int index, E element){
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
/**
* 往index位置添加元素
* @param index
* @param element
*/
public void add(int index, E element){
//边界判断
rangeCheckForAdd(index);
if (index == 0){// 给空链表添加第一个元素的情况
//创建新结点,first结点指向这个新结点
first = new Node<>(element,first);
}else {
//index上一个结点
Node<E> prev = node(index - 1);
//创建新结点,prev结点指向这个新结点
prev.next = new Node<>(element, prev.next);
}
size++;
}
/**
* 删除index位置对应的元素
* @param index
* @return
*/
public E remove(int index){
//边界判断
rangeCheck(index);
//记录结点
Node<E> node = first;
if (index == 0){ // 删除第一个元素是特殊情况
first = first.next;
}else {
//index上一个结点
Node<E> prev = node(index - 1);
//记录删除的结点
node = prev.next;
//node结点下一个结点,prev结点指向这个结点
prev.next = node.next;
}
size --;
return node.element;
}
/**
* 查看元素的位置
* @param element
* @return
*/
public int indexOf(E element){
// 取出头结点
Node<E> node = first;
if(element ==null){// 当element为null时的处理
// 遍历结点, 找到存储为null的结点, 返回索引
for (int i = 0; i < size; i++) {
if (node.element == null) return i;
node = node.next;
}
}else {
// 遍历结点, 找到存储的元素与指定元素相等的结点, 返回索引
for (int i = 0; i < size; i++) {
if (element.equals(node.element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 清除所有元素
*/
public void clear(){
size = 0;
first = null;
}
/**
* 获取index位置对应的结点对象
* @param index
* @return
*/
private Node<E> node(int index){
rangeCheck(index);
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
/**
* 打印
* @return
*/
@Override
public String toString() {
//Size = ? , [1, 2, 3]
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0){
string.append(", ");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
}
虚拟头结点 --- 存在内存问题
有时候为了让代码更加精简,统一所有结点的处理逻辑,可以在最前面增加一个虚拟头结点(不存储数据)
带虚拟头结点的单向链表与普通单向链表代码类似:但是 add、remove略有不同;
/**
* 增加一个虚拟头结点
* @param <E> 泛型
*/
public class VirtualHeadLinkedList<E> extends AbstractList<E>{
/**
* 元素所在的结点
*/
private Node<E> first;
/**
* 内部类 结点
* @param <E>
*/
private static class Node<E>{
E element;//元素
Node<E> next;//下个元素的地址
public Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
/**
* 构造函数 创建一个空结点 --- 增加
*/
public VirtualHeadLinkedList() {
this.first = new Node<>(null,null);
}
/**
* 返回index位置对应的元素
* @param index
* @return
*/
public E get(int index){
return node(index).element;
}
/**
* 设置index位置的元素
* @param index
* @param element
* @return
*/
public E set(int index, E element){
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
/**
* 往index位置添加元素 --- 修改
* @param index
* @param element
*/
public void add(int index, E element){
//边界判断
rangeCheckForAdd(index);
Node<E> prev = index ==0 ? first : node(index - 1);
prev.next = new Node<>(element, prev.next);
size++;
}
/**
* 删除index位置对应的元素 --- 修改
* @param index
* @return
*/
public E remove(int index){
//边界判断
rangeCheck(index);
Node<E> prev = index == 0 ? first : node(index - 1);
Node<E> node = prev.next;
prev.next = node.next;
size --;
return node.element;
}
/**
* 查看元素的位置
* @param element
* @return
*/
public int indexOf(E element){
// 取出头结点
Node<E> node = first;
if(element ==null){// 当element为null时的处理
// 遍历结点, 找到存储为null的结点, 返回索引
for (int i = 0; i < size; i++) {
if (node.element == null) return i;
node = node.next;
}
}else {
// 遍历结点, 找到存储的元素与指定元素相等的结点, 返回索引
for (int i = 0; i < size; i++) {
if (element.equals(node.element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 清除所有元素
*/
public void clear(){
size = 0;
first = null;
}
/**
* 获取index位置对应的结点对象 --- 修改
* @param index
* @return
*/
private Node<E> node(int index){
rangeCheck(index);
//first的下一个结点
Node<E> node = first.next;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
/**
* 打印 --- 修改
* @return
*/
@Override
public String toString() {
//Size = ? , [1, 2, 3]
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
Node<E> node = first.next;
for (int i = 0; i < size; i++) {
if (i != 0){
string.append(", ");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
}
动态数组、链表复杂度分析
数组的随机访问速度非常快:elements[n]的效率与 n 是多少无关;
双向链表
- 单向链表只能通过Node中next属性从头遍历链表,完成搜索
- 双向链表中的Node增加prev属性,指向该结点上一个结点
- 双向链表查找元素可以从first或last两个方向开始查找
因此双向链表可以提升链表的综合性能
双向链表只有一个结点(元素)
双向链表接口设计
相较于单向链表,双向链表需要重写查找结点、插入结点、删除结点、清空结点四个方法
- 新增一个last元素,最后一个元素
- 私有结点Node 中的prev 元素用于指向链表中的上一个结点
public class LinkedList<E> extends AbstractList<E> {
/**
* 元素所在的结点
*/
private Node<E> first;
//增加last结点
private Node<E> last;
/**
* 内部类 结点
* @param <E>
*/
private static class Node<E>{
E element;//元素
Node<E> prev;//增加上一个元素地址
Node<E> next;//下个元素的地址
public Node(E element, Node<E> prev,Node<E> next) {
this.element = element;
this.prev = prev;
this.next = next;
}
}
}
查找结点 - node(int index)
通过判断查找的节点在链表的前半段或后半段,选择first结点从前往后遍历或last结点从后往前遍历
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 获取index位置对应的节点对象
* @param index
* @return
*/
private Node<E> node(int index){
rangeCheck(index);
//往后
if (index < (size >> 1)){
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}else {//往前
Node<E> node = last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
return node;
}
}
......
}
清空元素 - clear()
需将新增属性last置为null
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 清除所有元素
*/
public void clear(){
size = 0;
first = null;
last = null;
}
......
}
添加元素 - add(int index, E element)
-
假定插入位置原结点为oldNode(下图2号结点),上一个结点为prevNode(下图1号结点),新结点为node
-
prevNode的next现在为node
-
oldNode的prev现在为node
-
node的prev即为oldNode的prev
-
node的next为oldNode
-
特殊情况,插入在最前面:
- 如果oldNode的prev为null,那么oldNode为原头结点。那么双向链表的first属性需改为node
-
插入在最后面:
- node的prev为双向链表的last,next为null,同时last需改为node
- 如果原链表为空,则双向链表的first和last都为node
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 往index位置添加元素
* @param index
* @param element
*/
public void add(int index, E element){
//边界判断
rangeCheckForAdd(index);
// size == 0
// index == 0
if (index == size){//往最后添加元素
Node<E> oldLast = last;
last = new Node<>(element,oldLast,null);
if (oldLast == null){//这是链表的第一个元素
first = last;
}else {
oldLast.next = last;
}
}else {// 正常添加元素
//插入位置的原结点,即为新节点的next节点
Node<E> next = node(index);
//新添加结点的上一个节点,即为该位置原节点的上一个节点
Node<E> prev = next.prev;
//创建新结点
Node<E> node = new Node<>(element,prev,next);
//原结点的上一个结点,为新添加结点
next.prev = node;
if (prev == null){//index == 0 即添加的是第一个
first = node;
}else {
//原结点上一个结点的next,即为新添加节点
prev.next = node;
}
}
size++;
}
......
}
删除元素 - remove(int index)
- 删除结点, 只需要让被删除结点的前一个结点与后一个结点之间链接, 同时去掉被删除结点引用
- 需要注意的是, 第0个结点和最后一个结点要特殊处理
public class LinkedList<E> extends AbstractList<E>{
......
/**
* 删除index位置对应的元素
* @param index
* @return
*/
public E remove(int index){
//边界判断
rangeCheck(index);
//删除的结点
Node<E> node = node(index);
//删除结点的上一个结点
Node<E> prev = node.prev;
//删除结点的下一个结点
Node<E> next = node.next;
//删除结点, 只需去掉对这个结点的引用
if (prev == null){//index == 0 即删除的是第一个
first = next;
}else {
prev.next = next;
}
if (next == null){//index == size - 1 即删除的是最后一个
last = prev;
}else {
next.prev = prev;
}
size --;
return node.element;
}
......
}
完整代码 --- 双向链表
/**
* 双向链表的实现
* @param <E> 泛型
*/
public class LinkedList<E> extends AbstractList<E> {
/**
* 元素所在的结点
*/
private Node<E> first;
//增加last节点
private Node<E> last;
/**
* 内部类 结点
* @param <E>
*/
private static class Node<E>{
E element;//元素
Node<E> prev;//增加上一个元素地址
Node<E> next;//下个元素的地址
public Node(E element, Node<E> prev,Node<E> next) {
this.element = element;
this.prev = prev;
this.next = next;
}
}
/**
* 返回index位置对应的元素
* @param index
* @return
*/
public E get(int index){
return node(index).element;
}
/**
* 设置index位置的元素
* @param index
* @param element
* @return
*/
public E set(int index, E element){
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
/**
* 往index位置添加元素
* @param index
* @param element
*/
public void add(int index, E element){
//边界判断
rangeCheckForAdd(index);
// size == 0
// index == 0
if (index == size){//往最后添加元素
Node<E> oldLast = last;
last = new Node<>(element,oldLast,null);
if (oldLast == null){//这是链表的第一个元素
first = last;
}else {
oldLast.next = last;
}
}else {// 正常添加元素
//插入位置的原结点,即为新节点的next节点
Node<E> next = node(index);
//新添加结点的上一个节点,即为该位置原节点的上一个节点
Node<E> prev = next.prev;
//创建新结点
Node<E> node = new Node<>(element,prev,next);
//原结点的上一个结点,为新添加结点
next.prev = node;
if (prev == null){//index == 0 即添加的是第一个
first = node;
}else {
//原结点上一个结点的next,即为新添加节点
prev.next = node;
}
}
size++;
}
/**
* 删除index位置对应的元素
* @param index
* @return
*/
public E remove(int index){
//边界判断
rangeCheck(index);
//删除的结点
Node<E> node = node(index);
//删除结点的上一个结点
Node<E> prev = node.prev;
//删除结点的下一个结点
Node<E> next = node.next;
//删除结点, 只需去掉对这个结点的引用
if (prev == null){//index == 0 即删除的是第一个
first = next;
}else {
prev.next = next;
}
if (next ==null){//index == size - 1 即删除的是最后一个
last = prev;
}else {
next.prev = prev;
}
size --;
return node.element;
}
/**
* 查看元素的位置
* @param element
* @return
*/
public int indexOf(E element){
// 取出头结点
Node<E> node = first;
if(element ==null){// 当element为null时的处理
// 遍历节点, 找到存储为null的节点, 返回索引
for (int i = 0; i < size; i++) {
if (node.element == null) return i;
node = node.next;
}
}else {
// 遍历节点, 找到存储的元素与指定元素相等的节点, 返回索引
for (int i = 0; i < size; i++) {
if (element.equals(node.element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 清除所有元素
*/
public void clear(){
size = 0;
first = null;
last = null;
}
/**
* 获取index位置对应的节点对象
* @param index
* @return
*/
private Node<E> node(int index){
rangeCheck(index);
//往后
if (index < (size >> 1)){
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}else {//往前
Node<E> node = last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
return node;
}
}
/**
* 打印
* @return
*/
@Override
public String toString() {
//Size = ? , [1, 2, 3]
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0){
string.append(", ");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
}
双向链表和单向链表对比
粗略对比一下删除的操作数量:操作数量缩减了近一半
有了双向链表,单向链表是否就没有任何用处了?
- 并非如此,在哈希表的设计中就用到了单链表
- 至于原因,在哈希表中会详细讲解
双向链表 vs 动态数组
动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间浪费(可以通过缩容解决)
双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费
- 如果频繁在尾部进行添加、删除操作,动态数组、双向链表均可选择
- 如果频繁在头部进行添加、删除操作,建议选择使用双向链表
- 如果有频繁的 (在任意位置)添加、删除操作,建议选择使用双向链表
- 如果有频繁的查询操作(随机访问操作),建议选择使用动态数组