前言
发一下牢骚,本来这个数据结构梳理的系列是在我找工作之前开始的,但是在中间找工作的过程中,一部分原因是面试太忙时间比较少,只能舍重就轻,当然更大的原因还是我自己的惰性,为了保持这个系列的完整性,以及自己日后的复习,于是决定重新开始,按照之前的思路,将这个系列梳理完,中间也会穿插一些关于这些数据结构在面试中的考题。
好了,上一篇讲的最基础的线性表的顺序表示,也就是基于数组,那么本篇的主要内容就是线性表的链式表示,简言之就是链表
目录
1、链表的概念
2、链表的基本操作
3、链表的优缺点
4、链表和数组的对比
5、单(双)向链表的完整代码
正文
1、链表的概念
用一组任意的存储单元存储线性表的数据元素的的数据结构。
很简单,值得一说的是,这个任意的存储单元,既可以是连续的,也可以是不连续的,在这个概念中的“单元”这个词,其实就是我们所熟知的节点,每个节点包含两个域,一个数据域,一个指针域或者两个指针域,如果是一个指针域,就是单向链表,如果是两个指针域,就是双向链表。
2、链表的基本操作
因为这里只是基本操作,所以就拿单向链表来举例子,双向链表只是比单向链表多了个指针域,只要把单向链表弄明白了,对应双向链表的基本操作就自然而然会了。
a.初始化
上一节说线性表的顺序表示时,也说过初始化,这里的初始化也大同小异,主要包括这样一些值的确定,一个是初始容量,另一个就是空节点的值。
空节点的值可以使用int
类型的默认值,也就是0,因为是链表,所以初始容量一般是0,还记得线性表初始化时的初始容量和负载因子这两个概念吗,这里之所以不需要这两个东东,是因为我们在声明一个数组的时候,必须要给它一个初始容量,而链表就比较灵活了,需要一个元素直接链上去就行。
由于是链表,另一个需要初始化确定的就是节点,因为这里以单向链表来说明,所以每个节点一个数据成员变量,一个next
指针成员变量,由于内部类的特性,所以节点类一般声明为内部类,如下
public class Node {
private int data;
private Node next;
public Node() {
}
public int getData() {
return data;
}
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
}
然后是单向链表类的初始化,按照上面描述的,如下
private Node head;//头结点
private Node tail;//尾节点
private int size = 0;//链表长度
public SingleLinkList() {
head = null;
tail = null;
size = 0;
}
public SingleLinkList(int data) {
head = new Node(data, null);
tail = head;
size++;
}
为了使用的方便,这里额外加了个使用一个元素值初始化链表的构造方法,不过这个不重要,看使用的需求即可,也可为了方便,自己加上两个、三个等元素的构造方法来初始化。
b.增加元素
增加元素为了增加的效率,这里分为三种情况,增加在链表头的位置,增加在链表尾的位置,增加在链表中间的位置。所以我们可以写三个对应的方法。其中相对复杂点的就是增加在链表中间的位置,对应的方法代码如下
public void addAtIndex(int data, int index) {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (head == null) {
addAtTail(data);
} else {
if (index == 0) {
addAtHead(data);
} else {
Node preNode = findNodeByIndex(index - 1);//找到前一个节点
preNode.next = new Node(data, preNode.next);
size++;
}
}
}
如果不是自己动手写的话,还是会发现即便只是一个增加代码,还是要考虑很多细节问题的,比如,一开始判断插入位置的合法性,以及链表的判空,这样就可以直接使用尾插来增加元素了,如果查找到应插在链表头,则使用头插来插入这个元素,否则,我们才手动进行元素的插入。因为我使用了Node
的构造方法,所以这里我只用了一行代码,其实也是最核心的代码,大致分为两步,第一步,将待插入节点的next
指针指向插入位置的next
指针指向的节点,第二步,将前一个节点的next
指针指向待插入节点。
然后再贴上头插和尾插的方法代码,头插的代码如下
public void addAtHead(int data) {
if (head == null) {
head = new Node(data, null);
tail = head;
} else {
Node newNode = new Node(data, head);
head = newNode;
}
size++;
}
尾插的代码如下
public void addAtTail(int data) {
if (head == null) {
head = new Node(data, null);
tail = head;
} else {
Node newNode = new Node(data, null);
tail.next = newNode;
// 将尾指针指向最新的最后一个节点
tail = newNode;
}
size++;
}
c.删除元素
删除元素,为了使用的方便性,也写了两个方法,一个是根据下标来删除,也就是从链表头开始的小标,另一种是根据元素值来删除。
首先我们来看根据下标来删除元素,代码如下
// 删除指定位置的节点
public void deleteByIndex(int index) {
if (head == null) {
System.out.println("链表为空");
} else {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
Node deleteNode = null;
if (index == 0)// 删除头节点
{
deleteNode = head;
head = head.next;
size--;
} else if (index == size - 1) {// 删除尾节点
deleteNode = tail;
Node prevNode = findNodeByIndex(index - 1);
prevNode.next = null;
tail = prevNode;
size--;
} else {
Node prevNode = findNodeByIndex(index - 1);// 获取要删除的节点的前一个节点
deleteNode = prevNode.next;// 要删除的节点就是prev的next指向的节点
prevNode.next = deleteNode.next;// 删除以后prev的next指向被删除节点之前所指向的next
size--;
}
deleteNode = null;
}
}
同样的,核心操作也只有三步,代码注释写的很清楚所以就不赘述了,但是为了健壮性和效率性,我们要尽可能的考虑到所有的情况,来优化我们的代码。
接下来是根据元素值来定向删除,代码如下
// 删除指定值的节点,如果存在这个节点,则删除,否则不作处理
public void deleteByData(int data) {
int index = findIndexByData(data);
if (index == -1) {
System.out.println("链表为空");
} else if (index == -2) {
System.out.println("未找到对应的元素");
} else {
deleteByIndex(index);
}
}
其核心就是调用了一个查找方法和上面的删除方法,查找方法后面会说到,先不急,我们主要是思路。当然为了效率, 我们也可以写一个尾删的方法,三行搞定
// 删除 链表中最后一个元素
public void deleteLast() {
deleteByIndex(size - 1);
}
所以我们只要弄懂了最核心的方法,其它的我们都可以去利用这个方法自由扩展。
d.查找元素
好了,到了最后一个基本操作,就是查找元素。同样的,根据我们平时使用的需求,主要是分为两种,根据下标查找对应的值,以及已知值查找对应的下标。
我们首先看第一种查找,根据下标查找对应的值,代码如下
// 通过index查找指定的节点
public Node findNodeByIndex(int index) {
if (head == null) {
return null;
}
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (index == 0) {
return head;
}
if (index == size - 1) {
return tail;
}
Node current = head;
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (i == index) {
return current;
}
}
return null;
}
首先是一连串的判断,然后来到一个核心的循环,由于是单向链表,所以只能从链表头开始一个个去遍历。弄明白了这个方法,那么第二种查找,根据值来查找对应的下标也就是小case了。代码如下
// 查找指定元素的位置
public int findIndexByData(int data) {
if (head == null) {
return -1;// 数组为空
}
if (head.data == data) {
return 0;
}
if (tail.data == data) {
return size - 1;
}
Node current = head;// 从第一个节点开始查找对比数据
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (current.data == data)
return i;
}
return -2;// 未找到对应的节点
}
要多说一下的是,如果值没有找到,我这里是返回的负数,来代表不同的含义,当然在实际中,最好是抛出异常处理,这样便于代码的异常排查。
3、链表的优缺点
在熟悉了链表的基本操作之后,我们可以很明显的发现一个问题,就是链表的插入和删除非常简单,但是链表的遍历就有点麻烦了,总感觉没有数组方便,数组直接定位,真快,但是链表还得从头开始去遍历,所以对于链表来说,优点是插入和删除效率高,但是查找效率低,相应的,链表适用于大量插入删除的场景,而对于一些需要频繁查找的场景就不那么适合了。
4、链表和数组的对比与合体
分析了一波链表的优缺点之后,我们再回想下数组,发现数组的优缺点和链表是正好反着的,数组是适用于查找,但是插入删除效率低,因为涉及到元素大量移动的问题,而链表正好弥补了数组的缺点,但是它却在查找方面表现不佳。
所以没有哪一种是完美的,或者说通用的,这二者的取舍需要我们根据实际的使用场景来自行选择。
那么我们是否想过这样一个问题,既然这两个互补,那我们能不能有一种东西把这两个对象的优点综合起来呢,这岂不是很完美了,对吧,当然,Java的设计者当然考虑过这个问题,于是他们设计了一个容器叫做HashMap
,嘿嘿嘿,想了解它的原理吗?想了解其中的奥秘吗?想知道优秀的Java设计者是如何综合数组和链表的优势的吗?限于篇幅,我就不详解了,这里我就丢下三个字:哈希表。剩下的就是你们自己去研究啦,hhhh…
5、单(双)向链表的完整代码
单向链表的完整代码如下
public class SingleLinkList {
public class Node {
private int data;
private Node next;
public Node() {
}
public int getData() {
return data;
}
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
}
private Node head;
private Node tail;
private int size = 0;
public SingleLinkList() {
head = null;
tail = null;
size = 0;
}
public Node getHead() {
return head;
}
public Node getTail() {
return tail;
}
public SingleLinkList(int data) {
head = new Node(data, null);
tail = head;
size++;
}
public int getLength() {
return size;
}
public void addAtHead(int data) {
if (head == null) {
head = new Node(data, null);
tail = head;
} else {
Node newNode = new Node(data, head);
head = newNode;
}
size++;
}
public void addAtTail(int data) {
if (head == null) {
head = new Node(data, null);
tail = head;
} else {
Node newNode = new Node(data, null);
tail.next = newNode;
// 将尾指针指向最新的最后一个节点
tail = newNode;
}
size++;
}
public void addAtIndex(int data, int index) {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (head == null) {
addAtTail(data);
} else {
if (index == 0) {
addAtHead(data);
} else {
Node preNode = findNodeByIndex(index - 1);
preNode.next = new Node(data, preNode.next);
size++;
}
}
}
// 通过index查找指定的节点
public Node findNodeByIndex(int index) {
if (head == null) {
return null;
}
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (index == 0) {
return head;
}
if (index == size - 1) {
return tail;
}
Node current = head;
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (i == index) {
return current;
}
}
return null;
}
// 查找指定元素的位置
public int findIndexByData(int data) {
if (head == null) {
return -1;// 数组为空
}
if (head.data == data) {
return 0;
}
if (tail.data == data) {
return size - 1;
}
Node current = head;// 从第一个节点开始查找对比数据
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (current.data == data)
return i;
}
return -2;// 未找到对应的节点
}
// 删除指定位置的节点
public void deleteByIndex(int index) {
if (head == null) {
System.out.println("链表为空");
} else {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
Node deleteNode = null;
if (index == 0)// 删除头节点
{
deleteNode = head;
head = head.next;
size--;
} else if (index == size - 1) {// 删除尾节点
deleteNode = tail;
Node prevNode = findNodeByIndex(index - 1);
prevNode.next = null;
tail = prevNode;
size--;
} else {
Node prevNode = findNodeByIndex(index - 1);// 获取要删除的节点的前一个节点
deleteNode = prevNode.next;// 要删除的节点就是prev的next指向的节点
prevNode.next = deleteNode.next;// 删除以后prev的next指向被删除节点之前所指向的next
size--;
}
deleteNode = null;
}
}
// 删除指定值的节点,如果存在这个节点,则删除,否则不作处理
public void deleteByData(int data) {
int index = findIndexByData(data);
if (index == -1) {
System.out.println("链表为空");
} else if (index == -2) {
System.out.println("未找到对应的元素");
} else {
deleteByIndex(index);
}
}
// 删除 链表中最后一个元素
public void deleteLast() {
deleteByIndex(size - 1);
}
// 更新指定位置的值
public void updateByIndex(int data, int index) {
findNodeByIndex(index).data = data;
}
// 清除链表中所有的元素
public void clear() {
head = null;
tail = null;
size = 0;
}
// 判断链表是否为空
public boolean isEmpty() {
return size == 0;
}
// 链表的输出 重写toString方法
public String toString() {
if (isEmpty()) {
return "null";
} else {
StringBuilder sb = new StringBuilder("");
for (Node current = head; current != null; current = current.next)// 从head开始遍历
{
sb.append(current.data + "-");
}
int len = sb.length();
return sb.delete(len - 1, len).append("").toString();// 删除最后一个 -
}
}
public static void main(String[] args) {
SingleLinkList list = new SingleLinkList();
list.addAtHead(1);
list.addAtHead(2);
list.addAtHead(3);
list.addAtTail(4);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.addAtIndex(9, 3);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.deleteByIndex(4);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.deleteByData(2);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.deleteLast();
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.addAtTail(5);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.addAtTail(5);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.updateByIndex(66, 3);
System.out.println(list+" size="+list.getLength()+" head="+list.getHead().getData()+" tail="+list.getTail().getData());
list.clear();
System.out.println(list+" size="+list.getLength());
}
}
同理,我们可以稍加扩展,写出双向链表的代码,如下
public class DoubleLinkList {
public class Node {
private int data;
private Node pre;
private Node next;
public Node() {
}
public int getData() {
return data;
}
public Node getPre() {
return pre;
}
public Node(int data, Node pre, Node next) {
this.data = data;
this.pre = pre;
this.next = next;
}
}
private Node head;
private Node tail;
private int size = 0;
public DoubleLinkList() {
head = null;
tail = null;
size = 0;
}
public Node getHead() {
return head;
}
public Node getTail() {
return tail;
}
public DoubleLinkList(int data) {
head = new Node(data, null, null);
tail = head;
size++;
}
public int getLength() {
return size;
}
public void addAtHead(int data) {
if (head == null) {
head = new Node(data, null, null);
tail = head;
} else {
Node newNode = new Node(data, null, head);
head = newNode;
}
size++;
}
public void addAtTail(int data) {
if (head == null) {
head = new Node(data, null, null);
tail = head;
} else {
Node newNode = new Node(data, tail, null);
tail.next = newNode;
// 将尾指针指向最新的最后一个节点
tail = newNode;
}
size++;
}
public void addAtIndex(int data, int index) {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (head == null) {
addAtTail(data);
} else {
if (index == 0) {
addAtHead(data);
} else {
Node preNode = findNodeByIndex(index - 1);
Node addNode = new Node(data, preNode, preNode.next);
preNode.next.pre = addNode;// 先设置后一个节点的前驱
preNode.next = addNode;// 再设置前一个节点的后继
size++;
}
}
}
// 通过index查找指定的节点
public Node findNodeByIndex(int index) {
if (head == null) {
return null;
}
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
if (index == 0) {
return head;
}
if (index == size - 1) {
return tail;
}
Node current = head;
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (i == index) {
return current;
}
}
return null;
}
// 查找指定元素的位置
public int findIndexByData(int data) {
if (head == null) {
return -1;// 数组为空
}
if (head.data == data) {
return 0;
}
if (tail.data == data) {
return size - 1;
}
Node current = head;// 从第一个节点开始查找对比数据
for (int i = 0; i < size & current.next != null; i++, current = current.next) {
if (current.data == data)
return i;
}
return -2;// 未找到对应的节点
}
// 删除指定位置的节点
public void deleteByIndex(int index) {
if (isEmpty()) {
throw new RuntimeException("链表为空");
} else {
if (index < 0 || index >= size) {
throw new RuntimeException("越界啦");
}
Node deleteNode = null;
if (index == 0)// 删除头节点
{
Node current = head.next;
current.pre = null;
deleteNode = head;
head = current;
size--;
} else if (index == size - 1) {// 删除尾节点
deleteNode = tail;
Node prevNode = findNodeByIndex(index - 1);
prevNode.next = null;
tail = prevNode;
size--;
} else {
Node prevNode = findNodeByIndex(index - 1);// 获取要删除的节点的前一个节点
deleteNode = prevNode.next;// 要删除的节点就是prev的next指向的节点
prevNode.next = deleteNode.next;// 删除以后prev的next指向被删除节点之前所指向的next
deleteNode.next.pre = prevNode;// 设置删除节点的后继节点的前驱等于 删除节点的前驱
size--;
}
deleteNode = null;
}
}
// 删除指定值的节点,如果存在这个节点,则删除,否则不作处理
public void deleteByData(int data) {
int index = findIndexByData(data);
if (isEmpty()) {
throw new RuntimeException("链表为空");
} else if (index == -2) {
System.out.println("未找到对应的元素");
} else {
deleteByIndex(index);
}
}
// 删除 链表中最后一个元素
public void deleteLast() {
deleteByIndex(size - 1);
}
// 更新指定位置的值
public void updateByIndex(int data, int index) {
findNodeByIndex(index).data = data;
}
// 判断链表是否为空
public boolean isEmpty() {
return size == 0;
}
// 清除链表中所有的元素
public void clear() {
head = null;
tail = null;
size = 0;
}
// 链表的输出 重写toString方法
public String toString() {
if (isEmpty()) {
return "null";
} else {
StringBuilder sb = new StringBuilder("");
for (Node current = head; current != null; current = current.next)// 从head开始遍历
{
sb.append(current.data + "-");
}
int len = sb.length();
return sb.delete(len - 1, len).append("").toString();// 删除最后一个 -
}
}
public static void main(String[] args) {
DoubleLinkList list = new DoubleLinkList();
list.addAtHead(1);
list.addAtHead(2);
list.addAtHead(3);
list.addAtTail(4);
list.addAtTail(5);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.addAtIndex(9, 0);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.deleteByIndex(2);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.deleteByData(4);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.deleteLast();
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.updateByIndex(55, 1);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.updateByIndex(44, 0);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
list.updateByIndex(66, 2);
System.out.println(list + " size=" + list.getLength() + " head=" + list.getHead().getData() + " tail="+ list.getTail().getData());
}
}
结语
好了,本篇就到此为止了,虽然内容有点多,但是原理都比较简单,主要是要自己动手,基本上手动实现一遍就差不多了,按照计划,下一篇应该是栈的梳理,下一篇见!!!