目录
1.ArrayList的缺陷
继上篇文章所讲顺序表超详解,我们已经了解了顺序表的使用,并进行简单的模拟实现。
由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。
2.链表
2.1 链表的概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的
2.2链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
虽然有这么多的链表的结构,但是我们重点掌握两种:
无头单向非循环链表:结构简单,一般不会单独用来存数据。
无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。
3.链表的实现(无头单向非循环)
3.1链表的定义
public static class Node {
int value;//值域
Node next;//指针指向下一个节点
public Node(int value) {
this.value = value;
}
}
public Node head; //定义一个head,便于记录
3.2头插法
顾名思义就是在链表的最前端插入新的元素
如图所示头插法的具体演示
public void addFirst(int data){
Node node = new Node(data);
node.next = head;
head = node;
}
3.3尾插法
如图尾插法具体演示
public void addLast(int data){
Node node = new Node(data);
//判断是不是空链表,若是空链表,将node赋给头节点head即可
if (head == null){
head = node;
return;
}
//设置变量current保存head,防止head丢失
Node current = head;
//循环找到最后一个节点
while(current.next != null){
current = current.next;
}
current.next = node;
}
3.3目标位置插入
1)首先检查所传入的下标是否合法
private void checkRangeIndex(int index) {
if (index < 0 || index > size()){
throw new IndexOutOfBoundsException("下表不合法,index = " + index);
}
}
2)我们需要找到具体插入的位置并记录
private Node findPrevNodeByIndex(int index) {
//记录头节点并进行遍历
Node current = head;
for (int i = 0; i < index - 1; i++) {
current = current.next;
}
return current;
}
3)找到位置我们需要判断是不是头节点或者尾节点还是中间节点,位置不同具体操作不同
public void addIndex(int index,int data){
//检查下标的合法性
checkRangeIndex(index);
// 此位置为链表头位置,用头插法即可
if (index == 0){
addFirst(data);
return;
}
if (index == size()) {
//此位置为链表尾位置,用尾插法即可
addLast(data);
return;
}
// 找到插入位置
Node prevIndex = findPrevNodeByIndex(index);
Node node = new Node(data);
//插入操作
node.next = prevIndex.next;
prevIndex.next = node;
}
3.4删除第一次出现的关键字
我们要遍历整个链表,来比较确定所要删除的第一个值
public void remove(int key){
//头节点就是要删除的节点
if(head.value == key){
head = head.next;
return;
}
Node current = head;
//(current.next != null)此条件判断是否走到最后一个节点
while(current.next != null){
if (current.next.value == key){
//删除操作
current.next = current.next.next;
return;
}
//没找到接着遍历
current = current.next;
}
}
3.5删除所有值为key的节点
如下图所示进行删除操作
public void removeAllKey(int key){
// 判断是不是空链表
if (head == null){
return;
}
//记录头节点和头节点的下一个节点
Node prevNode = head;
Node current = head.next;
while (current != null){
if (current.value == key){
//是删除节点,进行删除
prevNode.next = current.next;
}else {
// 不是删除节点,交换位置继续遍历
prevNode = current;
}
current = current.next;
}
//处理头节点
if (head.value == key){
head = head.next;
}
}
3.6链表的长度
链表的长度我们遍历求得即可,定义一个int类型的变量count记录,返回count即可
public int size(){
Node current = head;
int count = 0;
while (current != null){
count++;
current = current.next;
}
return count;
}
3.7清空链表
清空链表可以有两种方法
1)直接置头节点head = null即可
public void clear() {
head = null;
}
2)设置每个节点为空,遍历并操作,直至整个链表为空
public void clear() {
Node current = head;
while (current != null) {
Node nextNode = current.next;
current.next = null;
current = nextNode;
}
head = null;
}
3.8打印链表
public void display(){
StringBuilder sb = new StringBuilder();
sb.append("[");
Node current = head;
while(current != null){
sb.append(current.value);
if (current.next != null){
sb.append(",");
}
current = current.next;
}
sb.append("]");
System.out.println(sb.toString());
}
此时我们已经完成了无头双向非循环链表的实现,与顺序表相比,确实使用起来更加的灵活方便
接下来我们再介绍双向链表
4.双向链表(无头双向)
4.1链表的定义
双向链表顾名思义就是节点之间有两个连接点,一个前驱节点prev,一个后继节点next,相比于单向链表,多了前驱节点,如图所示
此时head记录链表头,tail记录链表尾
4.2头插法
如图所示的头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
// 如果是空链表插入
if(head == null){
head = node;
tail = node;
}else {
node.next = head;
head.prev = node;
head = node;
}
}
4.3尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
//空链表操作
if (head == null){
head = node;
}else {
tail.next = node;
node.prev = tail;
}
tail = node;
}
4.4目标位置插入
如题,各个节点地址的变化就是插入的操作
public void addIndex(int index, int data) {
// 检查下标的合法性
checkRangeForAdd(index);
//链表头直接用头插法
if(index == 0){
addFirst(data);
return;
}
// 链表尾直接用尾插法
if (index == size()){
addLast(data);
return;
}
ListNode tempNode = findNodeByIndex(index);
ListNode node = new ListNode(data);
node.next = tempNode;
node.prev = tempNode.prev;
node.prev.next = node;
node.next.prev = node;
}
// 找到插入位置
private ListNode findNodeByIndex(int index) {
ListNode current = head;
while(index > 0){
current = current.next;
index--;
}
return current;
}
private void checkRangeForAdd(int index) {
if (index < 0 || index > size()){
throw new IndexOutOfBoundsException("下标不合法,index = " + index);
}
}
4.4删除第一次出现的关键字
public void remove(int key){
if (head == null){
return;
}
ListNode current = head;
while (current != null){
//找到目标值
if(current.val == key){
//判断是不是头节点head,不同位置操作不同
if(current == head){
head = head.next;
if(head == null){
// 是头节点,并且链表为空
tail = null;
}else {
// 头节点并且链表不为空
head.prev.next = null;
head.prev = null;
}
}else if (current == tail){
// 判断删除的是尾节点操作
tail = tail.prev;
tail.next = null;
}else {
// 中间节点操作
current.next.prev = current.prev;
current.prev.next = current.next;
}
// 操作完成退出
return;
}
// 未完成继续遍历
current = current.next;
}
}
4.5删除所有值为key的节点
此方法和删除第一次出现的关键字的方法极为相似,只是这个找到第一个关键字删除后并不会退出,接着遍历直至走完。要注意的点是:如果要删除的关键字是头节点并且只剩下一个节点,要记录一下head,不然会报错
// 只剩一个节点
head.prev.next = null;
current = head;
head.prev = null;
如下是实现代码:
public void removeAllKey(int key){
if (head == null) {
return;
}
ListNode current = head;
while (current != null) {
// 头节点
if (current.val == key) {
if (current == head) {
head = head.next;
// 删除头节点
if (head == null) {
// 链表已经为空
tail = null;
} else {
// 只剩一个节点
head.prev.next = null;
current = head;
head.prev = null;
}
} else if (current == tail) {
// 处理尾节点
tail = tail.prev;
tail.next = null;
} else {
// 中间节点操作
current.next.prev = current.prev;
current.prev.next = current.next;
}
}
// 继续遍历
current = current.next;
}
}
4.6其余方法
剩下的方法就是和单链表实现相同,代码如下,不再过多解释
public int size() {
int count = 0;
ListNode current = head;
while(current != null){
count++;
current = current.next;
}
return count;
}
public void display() {
StringBuilder sb = new StringBuilder();
sb.append("[");
ListNode current = head;
while(current != null){
sb.append(current.val);
if (current.next != null){
sb.append(",");
}
current = current.next;
}
sb.append("]");
System.out.println(sb.toString());
}
public void clear(){
head = null;
}
在此我们已经介绍完双向链表。
5.单项链表和双向链表的区别
在存储空间方面:单链表需要的存储空间比双向链表的要少,因为双向链表不仅保存有指向下一个节点的指针,还保存有指向上一个节点的指针,需要较大的空间来存储双向链表的指针域。
在处理时间方面:双向链表的插入与删除操作比单链表的时间要快很多。在最末尾插入的时候,单链表需要找到需要插入位置的前一个节点,需要遍历整个链表,时间复杂度为O(n),而双向链表只需要head->tail,就可以得到最后一个节点的位置,然后就可以进行插入操作,时间复杂度为O(1)。在删除操作方面,单链表需要遍历到需要删除的节点的前一个节点,双向链表需要遍历到需要删除的节点,时间复杂度同为O(n)。
6.ArrayList 与 LinkedList区别
至此我们已经介绍完数据结构中arraylist 和 linkedlist 的相关知识。