链表(Linked List)
- 数据存储在”结点“(Node)中
- 优点:不用像动态数组/栈/队列那样依托于数组,依靠resize()来“动态”管理容量。真正的动态,不需要处理固定容量的问题。
- 缺点:丧失了随机访问的能力。
普通链表
主体
public class LinkedList<E> {
/**
* 定义私有内部类,外界不能直接访问。因为用户不需要清楚具体实现,
* 对于用户来说没有必要知道链表中还有Node结点这个东西,用户只要
* 知道怎么调用增删改查等方法就行。
*/
private class Node{
public E e;
//指向下一个结点的引用
public Node next;
//定义三种构造函数,应对三种不同的情况
public Node(E e,Node next){
this.e=e;
this.next=next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString(){
return e.toString();
}
}
//指向链表头部结点
private Node head;
private int size;
public LinkedList(){
head=null;
size=0;
}
}
add函数
//在链表头部添加结点
public void addFirst(E e){
head=new Node(e,head);
size++;
}
/**
* 在链表的index(0-based)位置(索引为index结点的前面)添加新的元素e
* 在链表中不是一个常用的操作,练习用
* 插入有两个步骤:
* 1、找到待插入位置前一个结点,并指向它(因为0前面没有结点,所以必须做特殊处理)
* 2、交换指向(把新结点插入进去)
*/
public void add(int index,E e){
if(index<0||index>size){
throw new IllegalArgumentException("add failed,illegal index");
}
if(index==0)
addFirst(e);
else{
Node prev=head;
for(int i=0;i<index-1;i++){
prev=prev.next;
}
/*Node node=new Node(e);
//下面两句位置不能交换
node.next=prev.next;
prev.next=node;*/
prev.next=new Node(e,prev.next);
size++;
}
}
//在链表末尾添加元素
public void addLast(E e){
add(size,e);
}
其他方法
//判断是否为空
public boolean isEmpty(){
return size==0;
}
//返回结点的个数
public int getSize(){
return size;
}
带有虚拟头结点的链表
- 在普通链表中添加一个指定位置的元素时,因为头结点前面没有元素,必须另做考虑。所以引申出在链表最前面添加一个虚拟头结点来解决这个问题,使逻辑统一。
主体
public class DummyHeadLinkedList<E> {
private class Node{
public E e;
public Node next;
//定义三种构造函数,应对三种不同的情况
public Node(E e,Node next){
this.e=e;
this.next=next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString(){
return e.toString();
}
}
//指向虚拟头结点的引用
private Node dummyHead;
private int size;
public DummyHeadLinkedList(){
//虚拟头结点中e=null
dummyHead=new Node();
size=0;
}
}
添加结点
- 因为有虚拟头结点,在0位置插入不用作特殊考虑
//因为索引为0的结点前面还有一个虚拟头结点,所以不用作特殊考虑
public void add(int index,E e){
if(index<0||index>size){
throw new IllegalArgumentException("add failed,illegal index");
}
Node prev=dummyHead;
//这里由index-1变成了index
for(int i=0;i<index;i++){
prev=prev.next;
}
prev.next=new Node(e,prev.next);
size++;
}
//在链表头部添加节点,复用add()
public void addFirst(E e){
add(0,e);
}
//在链表末尾添加元素
public void addLast(E e){
add(size,e);
}
删除结点
//删除链表第index个元素,在链表中这不是一个常用的操作,只作练习使用
/**
* 和add()一样要用到前一个结点,所以虚拟头结点又发挥作用了
* 1、找到待删除元素前一个元素,用prev指向它
* 2、用delNode指向待删除元素
* 3、prev.next=delNode.next; delNode.next=null
*/
public E remove(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("remove failed,Illegal index");
}
Node prev=dummyHead;
for(int i=0;i<index;i++){
prev=prev.next;
}
Node delNode=prev.next;
prev.next=delNode.next;
//让这个被删除的结点可以被回收
delNode.next=null;
size--;
return delNode.e;
}
//删除链表第一个结点
public E removeFirst(){
return remove(0);
}
//删除链表最后一个结点
public E removeLast(){
return remove(size-1);
}
// 从链表中删除元素e
public void removeElement(E e){
Node prev = dummyHead;
while(prev.next != null){
if(prev.next.e.equals(e))
break;
prev = prev.next;
}
if(prev.next != null){
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size --;
}
}
其他函数
//查找链表第index个元素,在链表中这不是一个常用的操作,只作练习使用
public E get(int index){
if(index<0||index>=size){
throw new IllegalArgumentException("get failed,index illegal");
}
Node cur=dummyHead;
for(int i=0;i<index+1;i++){
cur=cur.next;
}
return cur.e;
}
//更新链表第index个元素,在链表中这不是一个常用的操作,只作练习使用
public void set(int index,E e){
if(index<0||index>=size){
throw new IllegalArgumentException("get failed,index illegal");
}
Node cur=dummyHead;
for(int i=0;i<index+1;i++){
cur=cur.next;
}
cur.e=e;
}
//查找是否含有元素e
public boolean contains(E e){
Node cur=dummyHead.next;
while(cur!=null){
if(cur.e.equals(e))
return true;
cur=cur.next;
}
return false;
}
//获取链表第一个元素
public E getFirst(){
return get(0);
//return dummyHead.next.e;
}
//获取链表最后一个元素
public E getLast(){
return get(size-1);
}
@Override
public String toString(){
StringBuilder res=new StringBuilder();
res.append(String.format("LinkedList size:%d\n [",getSize()));
//两种遍历方法
/*Node cur=dummyHead.next;
for(int i=0;i<getSize();i++){
res.append(cur.e+"->");
cur=cur.next;
}*/
for(Node cur=dummyHead.next;cur!=null;cur=cur.next){
res.append(cur.e+"->");
}
res.append("null]");
return res.toString();
}
链表时间复杂度分析
- 对于链表来说最好只进行链表头部的增删查,最好不进行改操作。
链表栈
- 利用链表封装一个栈
- 链表栈和数组栈全部操作复杂度都是O(1),所以他们的性能差不多,但链表栈更节省空间。他们性能的差异在于数组栈的扩容缩容和链表栈频繁的new对象
public class LinkedListStack<E> implements Stack<E> {
//复用已经写好的带有虚拟头结点的链表
private DummyHeadLinkedList<E> linkedListStack;
public LinkedListStack(){
linkedListStack=new DummyHeadLinkedList<>();
}
//入栈
@Override
public void push(E e) {
linkedListStack.addFirst(e);
}
//出栈
@Override
public E pop() {
return linkedListStack.removeFirst();
}
//查看栈顶元素
@Override
public E peek() {
return linkedListStack.getFirst();
}
@Override
public int getsize() {
return linkedListStack.getSize();
}
@Override
public boolean isEmpty() {
return linkedListStack.isEmpty();
}
}
链表队列
头尾分析
- 上图中head和tail端插入结点都很容易
- 上图中head端删除结点很容易,但是tail端删除结点很难(需要从头结点遍历到tail前一个结点)
- 所以我们就把head端当作队列的队首(只删除结点),tail端当作队尾(只插入结点)
主体
public class LinkedListQueue<E> implements Queue<E> {
private class Node{
public E e;
public Node next;
public Node(E e,Node next){
this.e=e;
this.next=next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString(){
return e.toString();
}
}
//head指向队首结点,tail指向队尾结点
private Node head,tail;
private int size;
public LinkedListQueue(){
head=null;
tail=null;
size=0;
}
}
方法
@Override
public boolean isEmpty(){
return size==0;
}
@Override
public int getSize() {
return size;
}
//入队
@Override
public void enqueue(E e) {
//考虑队列为空添加结点的情况
if(tail==null){
tail=new Node(e);
head=tail;
}else{
tail.next=new Node(e);
tail=tail.next;
}
size++;
}
//出队
@Override
public E dequeue() {
if(isEmpty()){
throw new IllegalArgumentException("dequeue failed,queue is empty");
}
Node res=head;
//考虑删除尾结点后队列为空的情况
if(head==tail){
tail=null;
}
head=head.next;
//使要删除的队首结点彻底脱离链表,有利于回收
res.next=null;
size--;
return res.e;
}
@Override
public E getFront() {
if(isEmpty()){
throw new IllegalArgumentException("getFront failed,queue is empty");
}
return head.e;
}
时间复杂度
- 链表队列的操作都是O(1)
- 数组队列出队的操作是O(n),所以链表队列性能比数组队列好
- 循环队列(扩缩容操作耗能)和链表队列(频繁new对象耗能)性能差不多,但链表队列更节省空间
其他链表
双链表
能解决单链表在删除尾结点时间复杂度为O(n)的问题
循环链表
LinkedList底层就是用循环双链表实现的
数组链表
next用来保存下一个元素的下标,当为-1时表示尾结点,适用于已知链表的长度