目录
1.单向循环链表
单向循环链表:最后一个节点的next会指向头结点。
在原来的单向链表的基础上,我们实现一下单向循环链表。
2.1要改动的方法
1.add()方法:
- 插入头结点时,要维护循环指针
- 还要考虑只有1个节点时的特殊情况:自己指向自己。当size=0时,向index=0处插入。要找到插入位置的前一个节点
node(index-1);
,但是插入位置是index=0,那么就没有前一个节点,要做特殊处理。
- 代码修改:注意node()获取节点,一定要在first修改之前。
2.remove()方法
- 删除头结点时,要维护循环指针,让最后一个节点
node(size-1);
的next始终指向头结点。
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> oldNode = null;
if(index == 0) {
oldNode = first;
//拿到最后一个节点
Node<E> lastNode = node(size-1);
first = first.next;
lastNode.next = first;
}else {
rangeCheck(index);
Node<E> preNode = node(index-1);
oldNode = preNode.next;
preNode.next = preNode.next.next;
}
size--;
return oldNode.element;
}
- 当size=0时,边界检测
rangeCheck(index);
会报错。 - 当size=1时,这部分代码没有成功删除掉index=0的节点
if(index == 0) {
oldNode = first;
//拿到最后一个节点
Node<E> lastNode = node(size-1);
first = first.next;
lastNode.next = first;
}
- 修改
2.双向循环链表
双向循环链表的头结点的prev指针,指向尾节点;尾节点的next指针,指向头结点。
双向链表图示:
双向循环链表图示:
2.1.add和remove修改
在双向链表LinkedList的基础上做下修改
1.add
注意下一双向循环链表只有一个节点时的情况:这个唯一的节点的prev指针指向自己,next指针也指向自己。
双向链表只有一个节点时的情况:
插入尾部的情况:
代码:
2.remove
代码:
3.约瑟夫问题
3.1.增强双向循环链表
1.发挥循环链表的最大威力,增设1个成员变量,3个方法
- current:用于指向当前节点
private Node<E> current; //指向当前节点
- void reset():让current指向头结点first
/**
* 让current指向头结点first
*/
public void reset() {
current = first;
}
- E next():让current往后走一步,也就是current = current.next
/**
* 让current往后走一步,也就是current = current.next
* @return:返回当前的值
*/
public E next() {
if(current == null) return null;
current = current.next;
return current.element;
}
- E remove():删除current指向的节点,删除成功后让current指向下一个节点
/**
* 删除current指向的节点,删除成功后让current指向下一个节点
* @return
*/
public E remove() {
if(current == null) return null;
Node<E> next = current.next;
E element = remove(current);
if(size == 0) {
current = null;
}else {
current = next;
}
return element;
}
/**
* 根据索引删除节点
* @param index
* @return
*/
@Override
public E remove(int index) {
rangeCheck(index);
return remove(node(index));
}
/**
* 删除给定节点
* @param oldNode
* @return
*/
private E remove(Node<E> oldNode) {
if(size == 1) {
//因为改后的代码无法对size的情况进行删除,所以这边单独处理
first = null;
last = null;
}else {
Node<E> prevNode = oldNode.prev;
Node<E> nextNode = oldNode.next;
nextNode.prev = prevNode;
prevNode.next = nextNode;
//如果删除的是0结点
if(oldNode == first) { //index == 0
first = nextNode;
}
//如果删除的是size-1结点
if(oldNode == last) { //index == size -1
last = prevNode;
}
}
size--;
return oldNode.element;
}
3.2Josephus测试
public class Josephus {
public static void main(String[] args) {
CircleLinkedList<Integer> list = new CircleLinkedList<>();
for (int i = 1; i <= 8; i++) {
list.add(i);
}
//current指向头结点
list.reset();
while(!list.isEmpty()) {
list.next();//current后移一位
list.next();//current后移一位
System.out.println(list.remove());//删除current指向的节点
}
}
}
4.静态链表(了解)
-
1.我们之前所学习的链表是依赖于指针(引用)的实现的,但是有些变成语言是没有指针的,比如BASIC、Fortran语言。这种情况下,怎么实现链表呢?
-
2.我们可以通过数组来模拟链表,即静态链表:
数组的每个元素存放存放两个数据:值和下个元素的索引。
数组0下标位置存放头结点信息。
尾结点处的下一个元素的索引是-1。 -
3.怎么实现数组的每个元素存放两个值呢?
C语言可以通过结构体来实现,构造一个结构体数组。
其他语言可以使用两个数组来实现:A数组存放值,B数组的相同索引处存放下个元素的索引。
5.ArrayList的进一步优化
1.之前实现的ArrayList的一些缺点:
-
其实也是数组的一个通病:查询高效,修改低效。
-
因为有索引下标的存在,(下标相当于标注出来元素的位置)数组可以实现随机访问,每个数组元素的位置都可以通过索引算出来。因为为数组分配的是一篇连续的内存,所以只要有了下标,那么计算机就可以通过寻址公式:
a[i]_address = base_address + i * data_type_size
快速的定位到该元素的地址,进行访问。
即:数组支持随机访问,根据下标随机访问的时间复杂度为O(1)。
-
数组适合查找操作得到,但是查找的时间复杂度并不是O(1),即使是排好序的数组,你用二分查找,时间复杂度也是O(logn)。
-
当对数组进行插入操作时:该位置处的所有元素都要后移;当对数组进行删除操作时,该位置出的所有元素都需要前移。
2.怎么优化:提高插入和删除的效率
-
为什么要移动那么多元素:
因为我们锚定了数组的起始位置(0下标),而且数组的每个元素是连续的。那么我们
破坏
这个条件(但是要保证这个条件在逻辑上不变)
,就可以改善数组的插入和删除操作。 -
我们在逻辑上保证起始位置不变:用一个
int
变量存放数组真正的起始下标,锚定这个变量的值为起始位置。
每次删除头元素时,我们就让first变量存放头元素的下一个元素的索引,这样就不用移动后面的元素了 -
最多挪动一半的元素:
插入和删除时,不是永远挪动后面的元素,看看修改的位置两侧哪边的元素少,就挪动哪边,并且修改更新后的first的值,指向新的头元素。