1、链表
1.1、概念
数据存储在内存中一个个节点中,这些节点的内存地址不连续,是随机分散在内存中的,有头节点和尾节点,可以认为头节点的索引为0,头节点存储着下一个节点,也就是索引为1的节点的内存地址,索引为1的节点又存储下一个节点的内存地址,以此类推,尾节点中存储的内存地址为null。
1.2、常见链表
-
单向循环链表
在单向链表的基础上,尾节点的next指向头节点
-
双向循环链表
在双向链表的基础上,头节点的prev指向尾节点,尾节点的next指向头节点
2、单向链表
2.1、获取元素
在链表中,要获取元素,首先要得到元素所在的节点对象,因此必须要有相对应的获取节点对象的方法
private Node<E> node(int index){
rangeCheck(index);
Node<E> node = first;
for (int i = 0; i < index; i++){
node = node.next;
}
return node;
}
first中存储的是第一个节点的内存地址,要想获得第一个节点,就需要进行一次.next操作,要想获得第二个节点,就需要进行两次,编写for循环来获得想要得到的节点位置。
public E get(int index){
return node(index).element;
}
获取了该节点后,可以很轻松的获取到其中的元素。
2.2、改变元素
public E set(int index, E element){
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
与获取元素的方式一样,同样是得到该节点,然后改变这个节点的元素。
2.3、添加元素
public void add(int index, E element){
rangeCheckForAdd(index);
if ( index == 0){
//如果为0,则说明添加到第一个节点
first = new Node<>(element, first);
}else {
Node<E> preNode = node(index - 1);
preNode.next = new Node<>(element, preNode.next);
}
size++;
}
添加元素分为两种情况:
- 往第一个位置添加元素
只需要让first的箭头指向要添加的节点,再让要添加的节点的next指向之前的第一个节点即可
- 往其他位置添加元素
需要获取到前一个节点,将前一个节点的next赋值给要添加的节点的next,再将前一个节点的next指向要添加的节点即可。
2.4、删除元素
public E remove(int index){
rangeCheck(index);
Node<E> node = first;
if (index == 0){
//如果index==0,就说明该节点为第一个节点
first = first.next;
}else {
Node<E> preNode = node(index - 1);
node = preNode.next;
preNode.next = node.next;
}
size--;
return node.element;
}
删除元素还是分为两种情况
先获取前一个节点,通过前一个节点获取要删除的节点,再将要删除节点的next赋值给前一个节点。
3、双向链表
3.1、与单向链表的区别
双向链表与单向链表的区别就是多了一个指向前一个节点的属性prev,并且在first的基础上有了last指向最后一个节点,这样做可以提高效率,如果要检索前半部分的元素,从first开始遍历即可,如果要检索后半部分的元素,则从last开始,效率提升了一倍。
在JDK中自带的LinkedList也是双向链表
3.2、获取元素
正如上面所说,可以根据索引的位置判断从first找起还是从last找起。
private Node<E> node(int index){
rangeCheck(index);
//先判断这个节点的位置,如果在后半段则从last找起,前半段从first找起
Node<E> node = null;
if (index > (size >> 2)){
node = last;
for (int i = size - 1; i > index; i--){
node = node.pre;
}
}else {
node = first;
for (int i = 0; i < index; i++){
node = node.next;
}
}
return node;
}
3.3、添加元素
public void add(int index, E element){
rangeCheckForAdd(index);
//获得要插入位置的原节点,也就是插入节点的下一个节点
if (index == size){//当链表中没有元素或者往最后添加元素时
Node<E> oldLast = last;
last = new Node<E>(element, null, last);
if (oldLast == null){
first = last;
}else {
oldLast.next = last;
}
}else {
Node<E> next = node(index);
Node<E> pre = next.pre;
Node<E> node = new Node<>(element,next,pre);
if (pre == null){
first = node;
}else {
pre.next = node;
}
next.pre = node;
}
size++;
}
分为两种情况
-
往链表的最后一位添加元素
首先获取之前的last,然后让last指向要添加的节点,最后让之前的last的next指向要添加的节点。
不过要注意的是如果是往第一个位置添加元素,也就意味着这个元素是链表中的第一个元素,此时需要让first=last**(此时last已经指向了新添加的节点)**
-
往其他位置添加元素
首先获取原来位置的节点,通过该节点获取前一个节点,再将这两个节点赋值给要添加的节点的pre和next,最后再让前一个节点的next指向要添加的节点,让原来位置节点的pre指向要添加的节点即可。
需要注意的是如果前一个节点为null,也就意味着往第一个位置添加元素,因此这时候需要first指向要添加的节点。
3.4、删除元素
public E remove(int index){
rangeCheck(index);
Node<E> node = node(index);
Node<E> pre = node.pre;
Node<E> next = node.next;
if (pre == null){
first = next;
}else {
pre.next = next;
}
if (next == null){
last = pre;
}else {
next.pre = pre;
}
size--;
return node.element;
}
- 首先还是先获得要删除的节点以及前后两个节点
- 先不考虑删除头节点和尾节点,如果要删除一个节点,只需要让前一个节点的next指向要删除节点的下一个节点,让后一个节点的pre指向要删除节点的前一个节点。
- 再考虑特殊情况,删除头节点,头节点没有next,因此让first指向头节点的下一个节点,后面的操作与之前共用。
- 考虑删除尾节点,让last指向尾节点的前一个节点,后面的操作与之前共用。
5、单向循环链表
单向循环链表主要在添加和删除元素上面跟单向链表有一些区别
5.1、添加
public void add(int index, E element){
rangeCheckForAdd(index);
Node<E> newNode = new Node<>(element, first);
if ( index == 0){
//如果为0,则说明添加到第一个节点
if (size == 0){
newNode.next = newNode;
first = newNode;
}else {
Node<E> last = node(size - 1);
first = newNode;
last.next = first;
}
}else {
Node<E> preNode = node(index - 1);
newNode.next = preNode.next;
preNode.next = newNode;
}
size++;
}
只有往第一个位置添加元素时才与单向链表有区别,往其他位置添加元素并没有区别。
当往第一个位置添加元素时需要判断是否是第一个节点,如果是,则让该节点的next指向自身,如果不是,则获取最后一个节点,让最后一个节点的next指向要添加的节点。
5.2、删除元素
public E remove(int index){
rangeCheck(index);
Node<E> node = first;
if (index == 0){
//如果index==0,就说明该节点为第一个节点
if (size == 1){
first = null;
}else {
Node<E> last = node(size - 1);
first = first.next;
last.next = first;
}
}else {
Node<E> preNode = node(index - 1);
node = preNode.next;
preNode.next = node.next;
}
size--;
return node.element;
只有删除第一个节点时才与单向链表有区别。
需要让first指向要删除节点的下一个节点,让最后一个节点的next指向新的first即可。需要注意的是,如果链表中只剩下一个元素,需要另外处理,只需要让first指向null即可。这是因为最后一个节点也是第一个节点,如果让first指向下一个节点也就相当于指向自身,该节点依旧不会被删除。
6、双向循环链表
6.1、添加元素
public void add(int index, E element){
rangeCheckForAdd(index);
//获得要插入位置的原节点,也就是插入节点的下一个节点
if (index == size){//当链表中没有元素或者往最后添加元素时
Node<E> oldLast = last;
Node<E> newLast = new Node<E>(element, first, oldLast);
if (oldLast == null){
newLast.pre = newLast;
newLast.next = newLast;
first = newLast;
last = first;
}else {
oldLast.next = newLast;
first.pre = newLast;
last = newLast;
}
}else {
Node<E> next = node(index);
Node<E> pre = next.pre;
Node<E> node = new Node<>(element,next,pre);
next.pre = node;
pre.next = node;
if (pre == last){
first = node;
last.next = first;
}
}
size++;
}
添加元素时主要分为两种情况
-
往末尾添加
让之前尾节点的next指向要添加的节点,让头节点的prev指向要添加的节点,再让last指向要添加的节点。
**注意:**当要添加的节点是链表中的第一个节点时,该节点的pre以及next都指向其自身,并且first和last也都指向该节点。
-
往其他位置添加
大部分与双向链表一样,唯一的区别就是当往第一个位置添加元素时需要让最后一个节点的next指向该节点。
6.2、删除元素
public E remove(int index){
rangeCheck(index);
Node<E> node = node(index);
if (size == 1){
first = null;
last = null;
} else {
Node<E> pre = node.pre;
Node<E> next = node.next;
pre.next = next;
next.pre = pre;
if (pre == last){
first = next;
}
if (next == first){
last = pre;
}
}
size--;
return node.element;
}
- 先考虑最常见的情况,获得要删除节点的前后节点,改变前后节点的next和prev指向,即可成功删除元素。
- 当要删除的元素在头节点时,在之前的基础上,让first指向要删除节点的后一个节点即可。
- 当要删除节点在尾节点时,在之前的基础上,让last指向要删除节点的前一个节点即可。
- 需要注意的是,如果size为零,则需要让first和last都指向null才能删除最后一个节点,原因与上面单向循环链表所说的一样。