前面已经写过一篇使用数组实现的循环双端队列,当时说后续会写一下链表实现,现在补上,这个其实很早就写了,但是忘了发。。。
双向链表
大概介绍下双向链表的设计。
首先看下单向链表,我们只需要知道头结点就可以了,所以我们直接用就可以。
再来双向链表,它方向变多了,因此一个指针不够了,我们需要有两个指针,一个指向前面节点pre,一个指向后面节点next。这样一个双向链表节点就有了。
我们首先想一下为啥要有双向链表?
因为单链表有局限性。**我们常说链表插入的时间复杂度是O(1),这里的链表是指的双向链表,**如果是单向链表,在它前面插入元素的时间复杂度是O(n),但链表只能找到它后面的元素,因此你需要遍历整个链表找到它前面的那个元素。但是如果你这个节点是双向的,我就能在O(1)的时间找到前面的节点,那么时间复杂度就变成了O(1),这也是为啥工业上一般用的双向链表的原因。
对于双向链表最多的操作就是头尾节点插入和删除,所以自然想到需要定义两个双向链表节点head和tail,除了head和tail节点在加上链表初始因子和实际元素计数两个属性,这时候一个简单的双向链表就出来了,工业上为了使用方便一般会单独封装一个类这就是JAVA里的LinkedList。当然你也可以自己写两个变量,看你自己。我这里是自己写的两个变量头结点head,尾节点tail。元素添加就是放到head后面或者tail前面。
我一开就有一个疑问,为啥要往head和tail里面加元素而不是往外面加元素?
因为API的设计,双向链表都是往头和尾增删节点,往里面加元素你能很快的定义到头结点和尾结点进行操作,如果往外面加元素,那么你需要遍历链表找到头节点和尾结点在进行操作,麻烦很多。
思考
这道题的链表实现就是用双向链表,每个节点里面有前后两个指针。链表写法如果不熟的情况下会觉得很难,如果弄熟了其实会觉得比数组简单。因为不需要做那么多越界判断,它的判空和判满会比数组简单很多很多很多,但是相应的它也有自己的难点,就是节点的添加和删除,这块建议在写的时候画一下图,模拟一下,就会清晰很多。
首先是构建头尾两个节点,然后用一个size记录最大元素个数,一个count记录有多少元素。当在前面插入就是放在head后面,在后面插入就是放在tail前面。
对于插入节点,需要调整四个指针,前面的后指针,插入节点的前后指针,后面节点的前指针。插入时注意head.next=node
必须放到node.next=head.next
后面,不然head那条链就断了。整个后面都找不到了。
删除节点就简单很多,比如说删除头结点,只需要将头结点的下下个节点前指针指向自己,然后将头节点后指针指向下下节点,tail.pre.pre.next = tail; tail.pre = tail.pre.pre;
只需要改动两个地方,删除节点的头尾指针其实可以不动,因为你后面也用不到并找不到它了。
代码
/**
* 链表实现
*/
class MyCircularDeque {
private DoubleListNode head;
private DoubleListNode tail;
private int count;
private int size;
public MyCircularDeque(int k) {
this.size = k;
head = new DoubleListNode(0);
tail = new DoubleListNode(0);
head.next = tail;
tail.pre = head;
}
public boolean insertFront(int value) {
if (isFull()) {
return false;
}
DoubleListNode node = new DoubleListNode(value);
node.next = head.next;
head.next.pre = node;
head.next = node;
node.pre = head;
count++;
return true;
}
public boolean insertLast(int value) {
if (isFull()) {
return false;
}
DoubleListNode node = new DoubleListNode(value);
node.pre = tail.pre;
tail.pre.next = node;
tail.pre = node;
node.next = tail;
count++;
return true;
}
public boolean deleteFront() {
if (isEmpty()) {
return false;
}
head.next.next.pre = head;
head.next = head.next.next;
count--;
return true;
}
public boolean deleteLast() {
if (isEmpty()) {
return false;
}
tail.pre.pre.next = tail;
tail.pre = tail.pre.pre;
count--;
return true;
}
public int getFront() {
if (isEmpty()) {
return -1;
}
return head.next.val;
}
public int getRear() {
if (isEmpty()) {
return -1;
}
return tail.pre.val;
}
public boolean isEmpty() {
return count == 0;
}
public boolean isFull() {
return count == size;
}
class DoubleListNode{
private DoubleListNode pre;
private DoubleListNode next;
private int val;
public DoubleListNode(int val) {
this.val = val;
}
}
}
这里可以看到链表实现的判空就是count0,判满就是countsize,对比数组判空和判满是不是简单了特别多?第一次写的时候都给我震惊了。
最后强烈建议双向链表里的节点删除和添加建议多画图,画了图会清晰很多,然后看着写就行。
总结
链表这个实现没懂之前觉得很麻烦,懂了就会觉得很简单,这块没啥技巧,多练多思考就行。