关注公众号:EZ大数据(ID:EZ_DATA)。每天进步一点点,感觉很爽!
今天心情莫名的低调,可能是因为太帅了吧。
为了调整一波颓废的心态,我觉得还是学习是最好的良方。那就从链表开始……
那么今日目标有哪些呢?认识链表、链表的增删查改、链表来实现栈和队列。好,那就开始上正餐。
链表是真正的动态数据结构,我们前几天总结的动态数组、栈、队列底层依托于静态数组来实现,靠resize解决固定容量问题。
对于链表来说,是通过节点来装载元素,并通过节点与节点间连接起来的数据结构。如果我们想访问其中所有的元素,我们需要记录链表的头节点head。此外,链表中数据存储在“节点”Node中,其优点:真正的动态,不需要处理固定容量的问题,需要存储多少数据,就可以开辟响应的空间。同时缺点是:丧失了随机访问的能力。
那么数组和链表相比,二者有什么不同呢?对于数组来说,最好用于索引有语意的情况,优点:支持快速查询。相反,链表不适合用于索引有语意的情况,但链表的优点是:动态。
说了这么多理论,是不是有点小困,接下来我们来coding,增删查改操作一波。
现在我们来看链表中添加元素。首先在链表头部添加元素,原理是这样的:新添加的元素节点node.next=head当前的位置,然后再改变head的指向,head=node,这样的话,head就指向新添加的元素。具体示意图如下:
代码如下:
// 在链表头部添加新的元素e
public void addFirst(E e) {
Node node = new Node(e);
node.next = head;
head = node;
head = new Node(e, head);
size++;
add(0, e);
}
接下来呢,我们尝试链表中间添加元素,这个操作的关键是找到待添加的节点的前一个节点,我们用prev来表示。具体的原理就是,新添加的元素节点node.next=prev.next,然后再改变prev节点的指向,prev.next=node,这样就实现了中间插入元素。具体示意图如下:
此时,就有一个疑问,如果我们插入头部元素,那么头部节点head之前的节点就没有。那么,有什么方法可以通用呢?
答案就是我们可以虚拟个链表的头结点dummyhead。我们设置虚拟节点dummyhead,该节点不存储任何元素。示意图如下:
对于用户来说,dummyhead不存在,dummyhead.next=head节点。没有任何实际意义,只是为了我们编写逻辑方便。那么此时,我们就不需要对头结点添加元素进行特殊处理,只需要使用dummydead就可以。那么此时链表添加元素的代码,就可以写成:
// 在链表的index(0-based)位置添加新的元素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++) {
prev = prev.next;
}
prev.next = new Node(e, prev.next);
size++;
}
说完添加元素的操作,我们接下来看看删除元素的操作。在删除元素时,需要找到待删除元素delNode之前的元素节点prev,然后让prev.next=delNode.next,同时呢,需要资源释放,把delNode.next=null,示意图如下:
具体实现代码如下:
// 从链表中删除index(0-based)位置的元素,返回删除的元素
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;
}
现在我们来看看链表中查找元素的操作,首先获取链表的第index位置的元素,话不多说直接上code:
// 获取链表的第index个位置的元素
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;
}
对于判断链表中某个元素是否存在,比较EZ,就不贴代码,辱智商了……
最后,我们来看看链表修改元素的骚操作。
// 修改链表的第index个位置的元素e
public void set(int index, E e) {
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;
}
cur.e = e;
}
额……今天的代码简洁,高效。嗯,确实不难啊
代码搞完后,我们来简单分析一波时间复杂度。
对于添加元素来说,整体的时间复杂度是O(n)。
1.addFirst(e) O(1)
2.addLast(e) O(n)
3.add(index, e) O(n)
对于删除元素来说,整体的时间时间复杂度也是O(n)。
1.removeLast(e) O(n)
2.removeFirst(e) O(1)
3.remove(index, e) O(n)
由此可以看出,对于添加和删除元素来说,如果只对链表的头部进行元素操作时,时间复杂度是O(1),但从整体来看时间复杂度是O(n)。
那么对于查找元素的操作来说,整体的时间时间复杂度也是O(n)。
1.contains(e) O(n)
2.get(index, e) O(n)
当然,如果只查链表头部的话,时间复杂度为O(1)。
最后,对于修改操作来说,set(index, e) 其时间复杂度为O(n)。
今日汇总:
1.区别于数组,链表是动态的线性结构。
2.对于链表而言,我们需要继续头部节点head的位置,所以对头部元素的操作,其时间复杂度为O(1),而从整体上来看,操作链表的时间复杂度为O(n)。
3.通常我们在操作链表时,由于头部节点的特殊性,一般情况下,我们设置虚拟节点dummyhead。
4.对于链表的添加、删除操作方面,关于节点位置的改变,相对来说比较绕,需要多琢磨。
好了,今天的总结到此结束,额,由于今天事儿多,LeetCode就先不发文章了,当然我肯定刷题了,不容置疑……
明天我们来看看如何用链表来实现栈和队列,并比较下数组队列、循环队列和基于链表实现的队列,三者有何区别。
OK,拜了个拜~