文章目录
链表是最基础也是最重要的数据结构之一,它是真正的动态数据结构。如果有了对链表理解的基础将会更容易地学习更复杂地数据结构,比如二叉搜索树、AVL树、红黑树、B 树等等,这些都是上层建筑,而数组和链表才是结构基础。
一、数据结构链表的存储方式
数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)
而链表是不连续的,它是靠指针指向下一个元素的位置,所以不存在数组扩容的问题;如果知道某一个元素的前驱和后驱,操作指针就可删除该元素或则插入新的元素,时间复杂度为 O(1) 。
但是因为链表的存储空间不连续,就无法根据索引算出对应元素的地址,所以就不能随机访问;而且由于每个元素必须指向前后元素位置的指针,会消耗更多的内存空间。
在链表中,涉及到计算机领域重要地概念就是所谓地引用(在C++中叫做指针),其次链表本身就是一个线性数据结构,有清晰的递归结构,所以我们可以用循环的方式对链表进行操作。
二、什么是链表 Linked List ?
就是把数据存储在“节点”(Node)中。
//指向Node节点的下一个节点next
Class Node {
E e;
Node next;
}
对于链表,就像火车一样,每一个节点就是一节车厢,在车厢中存储正真的数据,而车厢和车厢直接进行连接以使得数据是整合在一起的,用户方便再所有的数据上进行查询等其它操作。而数据和数据之间的连接就是由 next 来完成的。
因为链表存储的数据是有限的,当节点存储的是一个 NULL ,表示这个节点是最后一个节点。所以链表是一个真正动态的数据结构,不需要处理固定容量的问题。 缺点就是不能随机访问。不能像数组那样,给一个索引,直接从数组拿出所需要的元素。
从底层机制上,数组所开辟的空间在内存里是连续分布的,所以能直接找到索引对应的偏移,直接计算出相应的数据所存储的内存地址,直接用 O(1) 的复杂读直接拿出来。但链表是靠 next
一层一层连接的,所以在底层每一个节点所在的内存的位置是不同的,我们必须靠 next
来找到所需的元素,比较麻烦。
下面我们用代码实现链表的存储节点:
public class LinkedList<E> {
private class Node{
public E e;
public Node next;
//1.传来e和next
public Node(E e,Node next){
this.e = e;
this.next = next;
}
//只传来e
public Node(E e){
this(e,null);
}
//什么都不传
public Node(){
this(null,null);
}
@Override
public String toString() {
//return e.toString();
return "Node{" +
"e=" + e +
", next=" + next +
'}';
}
}
}
如果我们要访问链表的所有节点,就要存储链表的头节点 head
。开始的时候,链表里没有元素,此时:
//初试时候为空
head = null;
size = 0;
获取链表中元素的个数:
public int getSize(){
return size;
}
返回当链表元素为空的时候:
public boolean isEmpty(){
return size == 0;
}
三、在链表中添加元素
在链表头添加元素:
public void addFirst(E e){
Node node = new Node(e);
node.next = head;
head = node;
//head = new Node(e,head);
size++;
}
在链表中间添加新的元素:
我们要找到插入元素的上一个节点位置,叫做 prev
,prev
初始位置是和 head
头节点指向同一个位置。
要找到插入元素的前一个节点,直接将它前一个节点的 next
指向要插入的元素,然后再将这个元素的 next
指向它上一个节点的后一个节点就完成添加操作。
关键点:找到要添加节点的前一个节点。
注意点(易错点):
//错误的方法!!!
prev.next = node;
node.next = prev.next;
代码实现:
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 = 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);
}
改进方法:为链表设立虚拟头节点
因为在链表头添加元素比较特殊,没有头节点,所以我们在头节点前面设一个虚拟的节点 null (不存任何元素),叫做 dummyHead。
那么现在的头节点表示为 head = dummyHead.next
获得链表的第index个位置的元素:
public E get(int index) {
if (index < 0 && index > size)
throw new IllegalArgumentException("Get failed. Illegal index");
Node curr = dummyHead.next;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
return curr.e;
}
获得链表的第一个元素:
public E getFirst() {
return get(0);
}
获得链表的最后一个元素:
public E getLast() {
return get(size - 1);
}
修改链表的第index个位置的元素e:
public void set(int index, E e) {
if (index < 0 && index > size)
throw new IllegalArgumentException("Set failed. Illegal index");
Node curr = dummyHead.next;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
curr.e = e;
}
查找链表中是否存在元素e:
public boolean contains(E e) {
Node curr = dummyHead.next;
while (curr != null) {
if (curr.e.equals(e)) {
return true;
}
curr = curr.next;
}
return false;
}
四、在链表中删除元素
寻找待删除元素的前一个节点,加入我们要删除索引为 2 位置的节点:
核心代码:
pre.next = delNode.next
delNode.next = null;
即进行了下面这个操作:
从链表中删除index位置的元素,并返回删除的元素
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;
}
从链表中删除第一个元素,并返回删除的元素
public E removeFirst(){
return remove(0);
}
从链表中删除最后一个元素
public E removeLast(){
return remove(size - 1);
}
五、链表的时间复杂度分析
添加操作 :O(n)
添加操作 | 时间复杂度 |
---|---|
链表头添加 | O(1) |
链表尾添加 | O(n) |
链表中添加 | O(n/2) = O(n) |
删除操作:O(n)
删除操作 | 时间复杂度 |
---|---|
链表头删除 | O(n) |
链表尾删除 | O(1) |
链表中删除 | O(n/2) = O(n) |
修改操作:O(n)
查找操作:O(n)
六、使用链表实现栈
public class LinkedListStack<E> implements Stack<E> {
private LinkedList<E> list;
public LinkedListStack() {
list = new LinkedList<>();
}
@Override
public int getSize() {
return list.getSize();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public void push(E e) {
list.addFirst(e);
}
@Override
public E pop() {
return list.removeFirst();
}
@Override
public E peek() {
return list.getFirst();
}
@Override
public String toString(){
StringBuffer res = new StringBuffer();
res.append("Stack:top ");
res.append(list);
return res.toString();
}
}
public interface Stack<E> {
int getSize();
boolean isEmpty();
void push(E e);
E pop();
E peek();
}
七、使用链表实现队列
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();
}
}
private Node head, tail;
private int size;
public LinkedListQueue(){
head = null;
tail = null;
size = 0;
}
@Override
public int getSize(){
return size;
}
@Override
public boolean isEmpty(){
return size == 0;
}
@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("Cannot dequeue from an empty queue.");
Node retNode = head;
head = head.next;
retNode.next = null;
if(head == null)
tail = null;
size --;
return retNode.e;
}
@Override
public E getFront(){
if(isEmpty())
throw new IllegalArgumentException("Queue is empty.");
return head.e;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Queue: front ");
Node cur = head;
//可以改写成for形式
while(cur != null) {
res.append(cur + "->");
cur = cur.next;
}
res.append("NULL tail");
return res.toString();
}
public static void main(String[] args){
LinkedListQueue<Integer> queue = new LinkedListQueue<>();
for(int i = 0 ; i < 10 ; i ++){
queue.enqueue(i);
System.out.println(queue);
if(i % 3 == 2){
queue.dequeue();
System.out.println(queue);
}
}
}
}
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(E e);
E dequeue();
E getFront();
}