作者:像一只狗,原文地址
什么是单链表Hello 继上次的 搞懂基本排序算法,这个一星期,我总结了,我所学习和思考的单链表基础知识和常见面试题,这些题有的来自 《剑指 offer》 ,有的来自《程序员代码面试指南》,有的来自 leetCode,不是很全面,但都具有一定代表性,相信大家看完以后一定跟我一样,对面试的时候算法题又多了一份自信。不过文章仍然是又臭又长,希望大家备好咖啡,火腿肠,方便面之类的,慢慢看,如果我有哪些理解不对的地方,也希望大家能在评论区为我指出,也算是对我码这么多字的认可吧。
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer),简单来说链表并不像数组那样将数组存储在一个连续的内存地址空间里,它们可以不是连续的因为他们每个节点保存着下一个节点的引用(地址),所以较之数组来说这是一个优势。
对于单链表的一个节点我们经常使用下边这种代码表示:
public class Node{
//节点的值
int value;
//指向下一个节点的指针(java 中表现为下一个节点的引用)
Node next;
public void Node(int value){
this.value = value;
}
}
单链表的特点
**1. 链表增删元素的时间复杂度为O(1),查找一个元素的时间复杂度为 O(n);
- 单链表不用像数组那样预先分配存储空间的大小,避免了空间浪费
- 单链表不能进行回溯操作,如:只知道链表的头节点的时候无法快读快速链表的倒数第几个节点的值。**
单链表的基本操作
上一节我们说了什么是单链表,那么我们都知道一个数组它具有增删改查的基本操作,那么我们单链表作为一种常见的数据结构类型也是具有这些操作的那么我们就来看下对于单链表有哪些基本操作:
由于单链表的存储地址不是连续的,链表并不具有直接获取链表长度的功能,对于一个链表的长度我们只能一次去遍历链表的节点,直到找到某个节点的下一个节点为空的时候得到链表的总长度,注意这里的出发点并不是一个空链表然后依次添加节点后,然后去读取已经记录的节点个数,而是已知一个链表的头结点然后去获取这个链表的长度:
public int getLength(Node head){
if(head == null){
return 0;
}
int len = 0;
while(head != null){
len++;
head = head.next;
}
return len;
}
查询指定索引的节点值或指定值得节点值的索引
由于链表是一种非连续性的存储结构,节点的内存地址不是连续的,也就是说链表不能像数组那样可以通过索引值获取索引位置的元素。所以链表的查询的时间复杂度要是O(n)级别的,这点和数组查询指定值得元素位置是相同的,因为你要查找的东西在内存中的存储地址都是不一定的。
/** 获取指定角标的节点值 */
public int getValueOfIndex(Node head, int index) throws Exception {
if (index < 0 || index >= getLength(head)) {
throw new Exception("角标越界!");
}
if (head == null) {
throw new Exception("当前链表为空!");
}
Node dummyHead = head;
while (dummyHead.next != null && index > 0) {
dummyHead = dummyHead.next;
index--;
}
return dummyHead.value;
}
/** 获取节点值等于 value 的第一个元素角标 */
public int getNodeIndex(Node head, int value) {
int index = -1;
Node dummyHead = head;
while (dummyHead != null) {
index++;
if (dummyHead.value == value) {
return index;
}
dummyHead = dummyHead.next;
}
return -1;
}
链表添加一个元素
学过数据结构的朋友一定知道链表的插入操作,分为头插法,尾插法,随机节点插入法,当然数据结构讲得时候也是针对一个已经构造好的(保存了链表头部节点和尾部节点引用)的情况下去插入一个元素,这看上去很简单,如果我们在只知道一个链表的头节点的情况下去插入一个元素,就不是那么简单了,就对于头插入法我们只需要构造一个新的节点,然后将这个节点的 next 指针指向已知链表的头节点就可以了。
1、 在已有链表头部插入一个节点
public Node addAtHead(Node head, int value){
Node newHead = new Node(value);
newHead.next = head;
return newHead;
}
2、在已有链表的尾部插入一个节点:
public void addAtTail(Node head, int value){
Node node = new Node(value);
Node dummyHead = head;
//找到未节点 注意这里是当元素的下一个元素为空的时候这个节点即为未节点
while( dummyHead.next != null){
dummyHead = dummyHead.next;
}
dummyHead.next = node;
}
3、在指定位置添加一个节点
// 注意这里 index 从 0 开始
public Node insertElement(Node head, int value, int index) throws Exception {
//为了方便这里我们假设知道链表的长度
int length = getLength(head);
if (index < 0 || index >= length) {
throw new Exception("角标越界!");
}
if (index == 0) {
return addAtHead(head, value);
} else if (index == length - 1) {
addAtTail(head, value);
} else {
Node pre = head;
Node cur = head.next;
//
while (pre != null && index > 1) {
pre = pre.next;
cur = cur.next;
index--;
}
//循环结束后 pre 保存的是索引的上一个节点 而 cur 保存的是索引值当前的节点
Node node = new Node(value);
pre.next = node;
node.next = cur;
}
return head;
}
在指定位置添加一个节点,首先我们应该找到这个索引所在的节点的前一个,以及该节点,分别记录这两个节点,然后将索引所在节点的前一个节点的 next 指针指向新节点,然后将新节点的 next 指针指向插入节点即可。与其他元素并没有什么关系,所以单链表插入一个节点时间复杂度为 O(1),而数组插入元素就不一样了如果将一个元素插入数组的指定索引位置,那么该索引位置以后元素的索引位置(内存地址)都将发生变化,所以一个数组的插入一个元素的时间复杂度为 O(n);所以链表相对于数组插入的效率要高一些,删除同理。
链表删除一个元素由于上边介绍了链表添加元素的方法这里对于链表删除节点的方法不在详细介绍直接给出代码:
1、 删除头部节点 也就是删除索引为 0 的节点:
public Node deleteHead(Node head) throws Exception {
if (head == null) {
throw new Exception("当前链表为空!");
}
return head.next;
}
2、 删除尾节点
public void deleteTail(Node head) throws Exception {
if (head == null) {
throw new Exception("当前链表为空!");
}
Node dummyHead = head;
while (dummyHead.next != null && dummyHead.next.next != null) {
dummyHead = dummyHead.next;
}
dummyHead.next = null;
}
3、 删除指定索引的节点:
public Node deleteElement(Node head, int index) throws Exception {
int size = getLength(head);
if (index < 0 || index >= size) {
throw new Exception("角标越界!");
}
if (index == 0) {
return deleteHead(head);
} else if (index == size - 1) {
deleteTail(head);
} else {
Node pre = head;
while (pre.next != null && index > 1) {
pre = pre.next;
index--;
}
//循环结束后 pre 保存的是索引的上一个节点 将其指向索引的下一个元素
if (pre.next != null) {
pre.next = pre.next.next;
}
}
return head;
}
由单链表的增加删除可以看出,链表的想要对指定索引进行操作(增加,删除),的时候必须获取该索引的前一个元素。记住这句话,对链表算法题很有用。