链表
本篇文章也是我学习网课而总结的一篇博文,网课地址链接《玩转算法系列–玩转数据结构》。
- 链表是一种线性结构,真正的动态数据结构。
- 数据存储是一种节点的形式。
- 具有真正的动态机制不需要担心容量的问题。
- 也丧失了随机访问的能力(并非绝对)
重要的组成部分–节点
一个Node中除了保存元素外还指向下一个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();
}
}
public Node next;
就是指向下一个节点的关键成员变量,如果是队尾则指向null。
成员变量
拥有一个头部节点指向目前链表结构的头部成员变量;
这里解释一个知识:
虚拟头节点
在链表中我们创造了多少个Node时,我们都拥有一个指向head的指向变量。
但是如果我们添加一个虚拟的头节点,当我们每次往头部添加Node时,头部节点没有前一个节点逻辑上会复杂些许(这个说法有点点争议),就可以使用这个虚拟头节点,使我们的逻辑较为轻便。
同时也因为存在虚拟头节点,所以类似于查询、修改、删除、赋值操作时所赋予的index,就是所对应的index。
/**
* 虚拟头节点
*/
private Node dummyHead;
/**
* 长度
*/
private int size;
增、删、改、查
由于链表是一组指向的结构,并且在成员变量中拥有头部的指向,因此操作头部时是很高效的(这与数组是刚好相反的);
链表中对元素的更改主要在于对指向的维护,避免破坏整个链表的结构。
- 增加的方法:
/**
* 在链表的index(0-based)位置添加新的元素e
* @param index
* @param e
*/
public void add(int index, E e){
if(index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Illegal index.");
}
Node prev = dummyHead;
for(int i = 0 ; i < index ; i ++){
//循环获取index位置的
prev = prev.next;
}
//Node node = new Node(e);
//node.next = prev.next;
//prev.next = node.next;
//下面一行代码就可以代表上述三行代码
prev.next = new Node(e, prev.next);
size ++;
}
/**
* 在链表头添加新的元素e
* @param e
*/
public void addFirst(E e){
add(0, e);
}
/**
* 在链表末尾添加新的元素e
* @param e
*/
public void addLast(E e){
add(size, e);
}
- 查询的方法
/**
* 获得链表的第index(0-based)个位置的元素
* @param index
* @return
*/
public E get(int index){
if(index < 0 || index >= size){
throw new IllegalArgumentException("Get failed. Illegal index.");
}
Node cur = dummyHead.next;
for(int i = 0 ; i < index ; i ++) {
cur = cur.next;
}
return cur.e;
}
/**
* 获得链表的第一个元素
* @return
*/
public E getFirst(){
return get(0);
}
/**
* 获得链表的最后一个元素
* @return
*/
public E getLast(){
return get(size - 1);
}
/**
* 查找链表中是否有元素e
* @param e
* @return
*/
public boolean contains(E e){
Node cur = dummyHead.next;
while(cur != null){
if(cur.e.equals(e)){
return true;
}
cur = cur.next;
}
return false;
}
- 赋值的方法
/**
* 修改链表的第index(0-based)个位置的元素为e
* @param index
* @param e
*/
public void set(int index, E e){
if(index < 0 || index >= size){
throw new IllegalArgumentException("Set failed. Illegal index.");
}
Node cur = dummyHead.next;
for(int i = 0 ; i < index ; i ++){
cur = cur.next;
}
cur.e = e;
}
- 删除的方法:
/**
* 从链表中删除index(0-based)位置的元素, 返回删除的元素
* @param index
* @return
*/
public E remove(int index){
if(index < 0 || index >= size){
throw new IllegalArgumentException("Remove failed. Index is illegal.");
}
Node prev = dummyHead;
for(int i = 0 ; i < index ; i ++){
prev = prev.next;
}
Node retNode = prev.next;
prev.next = retNode.next;
retNode.next = null;
size --;
return retNode.e;
}
/**
* 从链表中删除第一个元素, 返回删除的元素
* @return
*/
public E removeFirst(){
return remove(0);
}
/**
* 从链表中删除最后一个元素, 返回删除的元素
* @return
*/
public E removeLast(){
return remove(size - 1);
}
/**
* 从链表中删除元素e
* @param 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;
}
}
时间复杂度分析
方法 | 时间复杂度 |
---|---|
add | O(n) |
addFirst | O(1) |
addLast | O(n) |
get | O(n) |
getFirst | O(1) |
getLast | O(n) |
set | O(n) |
contains | O(n) |
remove | O(n) |
removeFirst | O(1) |
removeLast | O(n) |
removeElement | O(n) |
总体而言:此链表的许多操作都是O(n)的时间复杂度,因此并不适合进行增删改查,但是链表中有一个特点,就是对头部操作的时间复杂度均为O(1),因此特别适合进行头部操作,对比数组链表不会造成大量的开销(数组扩/缩容时)。
小结:
链表的特点决定了其操作的特点,天然支持动态,是数组外一种很重要的数据结构。