链表的使用与实现
一、链表介绍
- 动态数组有明显的缺点:可能会造成内存的浪费
- 是否可以⽤多少申请多少内存:链表可以
- 链表是⼀种链式存储的线性表,所有元素的内存地址不⼀定是连续的
1.链表
- 头节点是列表开头的节点
- 尾节点的特征是其 next 引⽤为空(null)
- 当节点的地址为空时,链表终⽌。
2. Node节点
- 链表中的每⼀个内存块被称为节点Node,Node节点由两部分构成:
存储数据(可以是任何类型)
还需记录链上下⼀个节点的地址,即后继指针next,⽤来存储下⼀个节点的Node对
象
3. 链表的优缺点
- 数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据搬移,
所以时间复杂度是 O(n)。 - 在链表中插入和删除一个数据是非常快速的,我们只需要考虑相邻结点的指针改
变,所以对应的时间复杂度是 O(1)。
二、常用链表
- 最常见的链表结构,它们分别是:单链表、双向链表和循环链表。
1.单链表
1)每个节点只包含一个指针,即后继指针。
2)单链表有两个特殊的节点,即首节点和尾节点。用首节点地址表示整条链表,尾节点的后
继指针指向空地址null。
3)性能特点:插入和删除节点的时间复杂度为O(1),查找的时间复杂度为O(n)。
2.循环链表
1)除了尾节点的后继指针指向首节点的地址外均与单链表一致。
2)适用于存储有循环特点的数据,比如约瑟夫问题。
3.双向链表
1)节点除了存储数据外,还有两个指针分别指向前一个节点地址(前驱指针prev)和下一个
节点地址(后继指针next)。
2)首节点的前驱指针prev和尾节点的后继指针均指向空地址。
3)性能特点:
- 和单链表相比,存储相同的数据,需要消耗更多的存储空间。
- 插入、删除操作比单链表效率更高O(1)级别。
以删除操作为例,删除操作分为2种情况:给定数据值删除对应节点和给定节点地址删除节点。
◇第一种情况:单链表和双向链表都需要从头到尾进行遍历从而找到对应节点进行删除,时间复杂度为O(n)。
◇第二种情况:要进行删除操作必须找到前驱节点,单链表需要从头到尾进行遍历直到p->next = q,时间复杂度为O(n),而双向链表可以直接找到前驱节点,时间复杂度为O(1)。
◇对于一个有序链表,双向链表的按值查询效率要比单链表高一些。因为我们可以记录上次查找的位置p,每一次查询时,根据要查找的值与p的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据。
4.双向循环链表
1)在双向循环链表中,可见的不只有头指针head,还有尾节点end。这是和单链表的
区别。
2) 双向循环链表的头指针head的前一个节点指向end,尾节点end的后一个节点指向
head。
三、⾃定义单向链表
1. 设计接⼝
int size(); // 元素的数量
boolean isEmpty(); // 是否为空
int indexOf(E element); // 查看元素的位置
boolean contains(E element); // 是否包含某个元素
E get(int index); // 返回index位置对应的元素
E set(int index, E element); // 设置index位置的元素
void clear(); // 清除所有元素
void add(E element); // 添加元素到最后面
void add(int index, E element); // 往index位置添加元素
E remove(int index); // 删除index位置对应的元素
2. List接口
- 包含共性的方法
package day02;
public interface List<E> {
public int size();
public int indexOf(E element); // 查看元素的位置
public boolean contains(E element); // 是否包含某个元素
public E get(int index); // 返回index位置对应的元素
public E set(int index, E element); // 设置index位置的元素
public void clear(); // 清除所有元素
public void add(E element); // 添加元素到最后面
public void add(int index, E element); // 往index位置添加元素
public E remove(int index); // 删除index位置对应的元素
public boolean isEmpty(); // 是否为空
}
3. AbstractList类
package day02;
public abstract class AbstractList<E> implements List<E>{
protected int size;
protected static final int ELEMENT_NOT_FOUND=-1;
@Override
public int size() {
return size;
}
@Override
public boolean contains(E element) {
return indexOf(element)!=ELEMENT_NOT_FOUND;
}
@Override
public boolean isEmpty() {
return size==0;
}
@Override
public void add(E element) {
add(size,element);
}
protected void checkAddIndex(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("索引越界" + "允许范围 size:0 => " + (size) + " 当前索引:" + index);
}
}
protected void checkIndex(int index){
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("索引越界" + "允许范围 size:0 => " + (size - 1) + " 当前索引:" + index);
}
}
}
4. MyLinkedList类
package com.kkb.day02;
public class MyLinkedList<E> extends AbstractList<E>{
private Node<E> first;
private static class Node<E>{
Node<E> next;
E element;
public Node(Node<E> next, E element) {
this.next = next;
this.element = element;
}
}
@Override
public E get(int index) {
checkIndex(index);
return node(index).element;
}
private Node<E> node(int index){
checkIndex(index);
Node<E> node=first;
for (int i = 0; i <index; i++) {
node=node.next;
}
return node;
}
@Override
public E set(int index, E element) {
checkIndex(index);
Node<E> node=node(index);
E oldElement=node.element;
node.element=element;
return oldElement;
}
@Override
public void clear() {
size=0;
first=null;
}
@Override
public int indexOf(E element) {
Node<E> node=first;
if (element == null) {
//通过遍历找到下一个节点,判断元素是否相等,相等返回i即可
for (int i = 0; i < size; i++) {//遍历查找
if (node.element == null) {//找到为null返回其下标
return i;
}
node=node.next;
}
} else {
for (int i = 0; i < size; i++) {
if (element.equals(node.element)) {
return i;
}
node=node.next;
}
}
//没有找到元素返回-1
return ELEMENT_NOT_FOUND;
}
@Override
public void add(int index, E element) {
checkAddIndex(index);
if(index==0){
first=new Node(first,element);
}else{
Node<E> pre=node(index-1);
Node<E> next=pre.next;
pre.next=new Node(next,element);
}
size++;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("size:" + size + "->[");
Node<E> node=first;
for (int i = 0; i <size ; i++) {
if(i!=0){
sb.append(", ");
}
sb.append(node.element);
node=node.next;
}
sb.append("]");//拼接末尾
return sb.toString();//返回输出结果
}
@Override
public E remove(int index) {
checkIndex(index);
Node<E> oldnode=first;
if(index==0){
first=first.next;
}else{
Node<E> pre=node(index-1);
oldnode=pre.next;
pre.next=oldnode.next;
}
size--;
return oldnode.element;
}
}
四、 来一道力扣题吗?
1. 题目:删除链表中的节点
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为 要被删除的节点 。
2. 提示
- 链表至少包含两个节点。
- 链表中所有节点的值都是唯一的。
- 给定的节点为非末尾节点并且一定是链表中的一个有效节点。
- 不要从你的函数中返回任何结果。
3. 解答
- 题目比较简单,在这里就不赘述啦~
五、 总结
- 本篇主要是在讲链表,并手写了一个单链表的实现,主要是和上一讲的数组进行比较,链表的优点很明显,但也还存在着缺点
- 还有其他更多更高效的数据结构,会在接下来慢慢介绍
- 巩固自己的同时也希望可以帮到大家~