什么是单链表
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer),简单来说链表并不像数组那样将数组存储在一个连续的内存地址空间里,它们可以不是连续的因为他们每个节点保存着下一个节点的引用(地址),所以较之数组来说这是一个优势。
对于单链表的一个节点我们经常使用下边这种代码表示:
public class Node{
//节点的值
int value;
//指向下一个节点的指针(java 中表现为下一个节点的引用)
Node next;
public void Node(int value){
this.value = value;
}
}
单链表的特点链表增删元素的时间复杂度为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;
}
由单链表的增加删除可以看出,链表的想要对指定索引进行操作(增加,删除),的时候必须获取该索引的前一个元素。记住这句话,对链表算法题很有用。
单链表常见面试题
介绍了链表的常见操作以后,我们的目标是学习链表常见的面试题目,不然我们学他干嘛呢,哈哈~ 开个玩笑那么我们就先从简单的面试题开始:
寻找单链表的中间元素
同学们可能看到这道面试题笑了,咋这么简单,拿起笔来就开始写,遍历整个链表,拿到链表的长度len,再次遍历链表那么位于 len/2 位置的元素就是链表的中间元素。
咱也不能说这种方法不对,想想一下一个腾讯的面试官坐在对面问这个问题,这个回答显然连自己这一关都很难过去。那么更渐快的方法是什么呢?或者说时间复杂度更小的方法如何实现这次查找?这里引出一个很关键的概念就是 快慢指针法,这也是面试官想考察的。
假如我们设置 两个指针 slow、fast 起始都指向单链表的头节点。其中 fast 的移动速度是 slow 的2倍。当 fast 指向末尾节点的时候,slow 正好就在中间了。想想一下是不是这样假设一个链表长度为 6 , slow 每次一个节点位置, fast 每次移动两个节点位置,那么当fast = 5的时候 slow = 2 正好移动到 2 的节点的位置。
所以求解链表中间元素的解题思路是:
public Node getMid(Node head){
if(head == null){
return null;
}
Node slow = head;
Node fast = head;
// fast.next = null 表示 fast 是链表的尾节点
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
判断一个链表是否是循环链表
首先此题也是也是考察快慢指针的一个题,也是快慢指针的第二个应用。先简单说一下什么循环链表,循环链表其实就是单链表的尾部指针指向头指针,构建成一个环形的链表,叫做循环链表。 如 1 -> 2 - > 3 -> 1 -> 2 .....。为什么快慢指针再循环链表中总能相遇呢?你可以想象两个人在赛跑,A的速度快,B的速度慢,经过一定时间后,A总是会和B相遇,且相遇时A跑过的总距离减去B跑过的总距离一定是圈长的n倍。这也就是 Floyd判环(圈)算法。
那么如何使用快慢指针去判断一个链表是否为环形链表呢:
private static boolean isLoopList(Node head){
if (head == null){
return false;
}
Node slow = head;
Node fast = head.next;
//如果不是循环链表那么一定有尾部节点 此节点 node.next = null
while(slow != null && fast != null && fast.next != null){
if (fast == slow || fast.next == slow){
return true;
}
// fast 每次走两步 slow 每次走一步
fast =fast.next.next;
slow = slow.next;
}
//如果不是循环链表返回 false
return false;
}
已知一个单链表求倒数第 N 个节点
为什么这个题要放在快慢指针的后边呢,因为这个题的解题思想和快慢指针相似,我们可以想一下:如果我们让快指针先走 n-1 步后,然后让慢指针出发。快慢指针每次都只移动一个位置,当快指针移动到链表末尾的时候,慢指针是否就正处于倒数第 N 个节点的位置呢。
是这里把这两个指针称之为快慢指针是不正确的,因为快慢指针是指一个指针移动的快一个指针移动的慢,而此题中 快指针只是比慢指针先移动了 n-1 个位置而已,移动速度是相同的。
如果上边的讲解不好理解,这里提供另外一种思路,就是想象一下,上述快慢指针的移动过程,是否就相当于一个固定窗口大小为 n 的滑动窗口:n = 1 fast 指针不移动 fast 到达最后一个节点 即 fast.next 的时候 slow 也到达尾部节点满条件
n = len fast 指针移动 n-1(len -1 ) 次 fast 到达最后一个节点 slow 位于头节点不变 满足条件 两个临界值均满足我们这种假设。
1< n < len 的时候我们假设 n = 2 ,那么 fast 比 slow 先移动一步,也就是窗口大小为 2, 那么当 fast.next = null 即 fast 已经指向链表最后一个节点的时候,slow 就指向了 倒数第二个节点。
下面我们来看下函数实现:
/**
* 注意我们一般说倒数第 n 个元素 n 是从 1 开始的
*/
private Node getLastIndexNode(Node head, int n) {
// 输入的链表不能为空,并且 n 大于0
if (n < 1 || head == null) {
return null;
}
n = 10;
// 指向头结点
Node fast = head;
// 倒数第k个结点与倒数第一个结点相隔 n-1 个位置
// fast 先走 n-1 个位置
for (int i = 1; i < n; i++) {
// 说明还有结点
if (fast.next != null) {
fast = fast.next;
}else {
// 已经没有节点了,但是i还没有到达k-1说明k太大,链表中没有那么多的元素
return null;
}
}
Node slow = head;
// fast 还没有走到链表的末尾,那么 fast 和 slow 一起走,
// 当 fast 走到最后一个结点即,fast.next=null 时,slow 就是倒数第 n 个结点
while (fast.next != null) {
slow = slow.next;
fast = fast.next;
}
// 返回结果
return slow;
}
删除单链表的倒数第 n 个节点
看到这个题时候乐了,这考察的知识点不就是一道求解倒数第 n 个节点的进化版么。但是我们也说过,如果想操作链表的某个节点(添加,删除)还必须知道这个节点的前一个节点。所以我们删除倒数第 n 个元素就要找到倒数第 n + 1 个元素。然后将倒数第 n + 1个元素 p 的 next 指针 p.next 指向 p.next.next 。
我们找到倒数第 n 个节点的时候,先让 fast 先走了 n-1 步,那么我们删除倒数第 n 个节点的时候就需要 让 fast 先走 n 步,构建一个 n+1 大小的窗口,然后 fast 和 slow 整体平移到链表尾部,slow 指向的节点就是 倒数第 n+1 个节点。
这里我们还可以使用滑动窗口的思想来考虑临界值:n = 1 的时候我们需要构建的窗口为 2,也就是当 fast.next = null 的时候 slow 在的倒数第二个节点上,那么可想而知是满足我们的条件的。
当 1 < n < len 的时候我们总是能构建出这样的一个 len + 1大小的窗口,n 最大为 len -1 的时候,slow 位于头节点,fast 位于未节点,删除倒数第 n 个元素,即删除正数第二个节点,slow.next = slow.next.next 即可。
当 n > len 的时候可想而知,我们要找的倒数第 n 个元素不存在,此时返回 头节点就好了
n = len 的时候比较特殊,循环并没有因为倒数第 len 个元素不存在而终止,并进行了 fast = fast.next; 循环结束后 fast 指向 null , 且此时 slow 位于头节点,所以我们要删除的节点是头节点,只需要在循环结束后判断 如果 fast == null 返回 head.next 即可
下面我们来看解法:
/**
* 删除倒是第 n 个节点 我们就要找到倒数第 n + 1 个节点, 如果 n > len 则返回原列表
*/
private Node deleteLastNNode(Node head, int n) {
if (head == null || n < 1) {
return head;
}
Node fast = head;
//注意 我们要构建长度为 n + 1 的窗口 所以 i 从 0 开始
for (int i = 0; i < n; i++) {
//fast 指针指向倒数第一个节点的时候,就是要删除头节点
if (fast == null) {
return head;
} else {
fast = fast.next;
}
}
// 由于 n = len 再循环内部没有判断直接前进了一个节点,临界值 n = len 的时候 循环完成或 fast = null
if (fast == null){
return head.next;
}
//此时 n 一定是小于 len 的 且 fast 先走了 n 步
Node pre = head;
while (fast.next != null) {
fast = fast.next;
pre = pre.next;
}
pre.next = pre.next.next;
return head;
}
旋转单链表题目:给定一个链表,旋转链表,使得每个节点向右移动k个位置,其中k是一个非负数。 如给出链表为 1->2->3->4->5->NULL and k = 2, return 4->5->1->2->3->NULL.
做完,删除倒数第 n 个节点的题,我们在看着道题是不是很简单了,这道题的本质就是,找到 k 位置节点 将其变成尾节点,然后原来链表的尾节点指向原来的头节点
private Node rotateList(Node head, int n) {
int start = 1;
Node fast = head;
//先让快指针走 n 给个位置
while (start < n && fast.next != null) {
fast = fast.next;
start++;
}
//循环结束后如果 start < n 表示 n 整个链表还要长 旋转后还是原链表
//如果 fast.next = null 表示 n 正好等于原链表的长度此时也不需要旋转
if (fast.next == null || start < n) {
return head;
}
//倒数第 n + 1个节点
Node pre = fast;
//旋转后的头节点
Node newHead = fast.next;
while (fast.next != null) {
fast = fast.next;
}
//原链表的最后一个节点指向原来的头节点
fast.next = head;
//将旋转的节点的上一个节点变为尾节点
pre.next = null;
return newHead;
}
翻转单链表
翻转一个单链表,要求额外的空间复杂度为 O(1)
翻转单链表是我感觉比较难的基础题,那么先来屡一下思路:一个节点包含指向下一节点的引用,翻转的意思就是对要原来指向下一个节点引用指向上一个节点找到当前要反转的节点的下一个节点并用变量保存因为下一次要反转的是它
然后让当前节点的 next 指向上一个节点, 上一个节点初始 null 因为头结点的翻转后变为尾节点
当前要反转的节点变成了下一个要比较元素的上一个节点,用变量保存
当前要比较的节点赋值为之前保存的未翻转前的下一个节点
当前反转的节点为 null 的时候,保存的上一个节点即翻转后的链表头结点
ok,不知道按照上边我写的步骤能否理解一个链表的翻转过程。如果不理解自己动手画一下可能更好理解哈,注意在画的时候一次只考虑一个节点,且不要考虑已经翻转完的链表部分。
下面我们来看下实现过程:
public Node reverseList(Node head){
//头节点的上一个节点为 null
Node pre = null;
Node next = null;
while(head != null){
next = head.next;
head.next = pre;
pre = head;
head = next;
}
}
翻转部分单链表题目要求:要求 0 < from < to < len 如果不满足则不翻转
这类题还有一类进阶题型,就是翻转链表 from 位置到 to 位置的节点,其实翻转过程是相似的,只是我们需要找到位于 from 的前一个节点,和 to 的下一个节点 翻转完 from 和 to 部分后将 from 的上一个节点的 next 指针指向翻转后的to,将翻转后 from 节点的 next 指针指向 to 节点下一个节点。遍历整个链表 遍历过程需要统计链表的长度 len ,from 节点的前一个节点 fPosPre , 翻转开始的节点 from ,翻转结束的节点 to ,节点to 节点的后一个节点 tPosNext 。
循环后判断条件 0 < from < to < len 的条件是否满足,如果不满足返回 head
进行 from 到 to 节点翻转
翻转完后判断 如果翻转的起点不是 head 则返回 head,如果反转的链表是起点,那么翻转后 toPos 就是头结点。
下面我们开看代码(你可能有更简便的解法,省去几个变量,但是下面的解法应该是最好理解的);
private Node reversePartList(Node head, int from, int to) {
Node dummyHead = head;
int len = 0;
Node fPosPre = null;
Node tPosNext = null;
Node toPos = null;
Node fromPos = null;
while (dummyHead != null) {
//因为 len = 0 开始的所以 len 先做自增一
len++;
if (len == from) {
fromPos = dummyHead;
} else if (len == from - 1) {
fPosPre = dummyHead;
} else if (len == to + 1) {
tPosNext = dummyHead;
} else if (len == to) {
toPos = dummyHead;
}
dummyHead = dummyHead.next;
}
//不满足条件不翻转链表
if (from > to || from < 0 || to > len || from > len) {
return head;
}
Node cur = fromPos;
Node pre = tPosNext;
Node next = null;
while (cur != null && cur != tPosNext) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 如果翻转的起点不是 head 则返回 head
if (fPosPre != null) {
fPosPre.next = pre;
return head;
}
// 如果反转的链表是起点,那么翻转后 toPos 就是头结点
return toPos;
}
单链表排序
单链表的归并排序
归并的中心思想在于在于已知两个链表的时候,如果按顺序归并这两个链表。其实这也是一道面试题按照元素的大小合并两个链表那么我们就先看下如何合并两个链表 我们称这个过程为 merge 。
private Node merge(Node l, Node r) {
//创建临时空间
Node aux = new Node();
Node cur = aux;
//由于链表不能方便的拿到链表长度 所以一般使用 while l == null 表示链表遍历到尾部
while (l != null && r != null) {
if (l.value < r.value) {
cur.next = l;
cur = cur.next;
l = l.next;
} else {
cur.next = r;
cur = cur.next;
r = r.next;
}
}
//当有一半链表遍历完成后 另外一个链表一定只剩下最后一个元素(链表为基数)
if (l != null) {
cur.next = l;
} else if (r != null) {
cur.next = r;
}
return aux.next;
}
返回的 Node 节点为归并完成后的链表头节点。那么归并排序的核心过程也完成了,想想我们想要归并一个数组还需要一个划分操作 中心节点 mid 是谁,看到这里是不是笑了,之前我们已经讲过如何寻找一个链表的中间元素,那么是不是万事具备了,ok 我们来实现链表的归并排序:
private Node mergeSort(Node head) {
//递归退出的条件 当归并的元素为1个的时候 即 head.next 退出递归
if (head == null || head.next == null) {
return head;
}
Node slow = head;
Node fast = head;
//寻找 mid 值
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
Node left = head;
Node right = slow.next;
//拆分两个链表 如果设置链表的最后一个元素指向 null 那么 left 永远等于 head 这链表 也就无法排序
slow.next = null;
//递归的划分链表
left = mergeSort(left);
right = mergeSort(right);
return merge(left, right);
}
单链表的插入排序
回想一下数组的插入排序,我们从第二个数开始遍历数组,如果当前考察的元素值比下一个元素的值要大,则下一个元素应该排列排列在当前考察的元素之前,所以我们从已经排序的元素序列中从后向前扫描,如果该元素(已排序)大于新元素,将该元素移到下一位置(赋值也好,交换位置也好)。但是由于链表的不可回溯性,我们只能从链表的头节点开始找,这个元素应该要在的位置。
我们来看下代码实现:
public Node insertionSortList(Node head) {
if (head == null || head.next == null) return head;
Node dummyHead = new Node(0);
Node p = head;
dummyHead.next = head;
//p 的值不小于下一节点元素考察下一节点
while (p.next != null) {
if (p.value <= p.next.value) {
p = p.next;
} else {
//p 指向 4
Node temp = p.next;
Node q = dummyHead;
p.next = p.next.next;
//从头遍历链表找到比当前 temp 值小的第一个元素插入其后边 整个位置一定在 头节点与 q 节点之间
while (q.next.value < temp.value && q.next != q)
q = q.next;
temp.next = q.next;
//重新连接链表 注意 else 的过程并没有改变 p 指针的位置
q.next = temp;
}
}
return dummyHead.next;
}
划分链表题目 : 按某个给定值将链表划分为左边小于这个值,右边大于这个值的新链表 如一个链表 为 1 -> 4 -> 5 -> 2 给定一个数 3 则划分后的链表为 1-> 2 -> 4 -> 5
此题不是很难,就是遍历一遍链表,就可以完成,我们新建一两个链表,如果遍历过程中,节点值比给定值小则划在左链表中,反之放在右链表中,遍历完成后拼接两个链表就好。不做过多解释直接看代码。
private Node partition(Node head , int x){
if(head == null){
return = null;
}
Node left = new Node(0);
Node right = new Node(0);
Node dummyLeft = left;
Node dummyRight = right;
while(head != null){
if(head.value < x){
dummyLeft.next = head;
dummyLeft = dummyLeft.next;
}else{
dummyRight.next = head;
dummyRight = dummyRight.next;
}
head = head.next;
}
dummyLeft.next = right.next;
right.next = null;
return left.next;
}
链表相加求和题目: 假设链表中每一个节点的值都在 0-9 之间,那么链表整体可以代表一个整数。 例如: 9->3->7 可以代表 937 给定两个这样的链表,头节点为 head1 head2 生成链表相加的新链表。 如 9->3->7 和 6 -> 3 生成的新链表应为 1 -> 0 -> 0 -> 0
此题如果明白题意的情况并不难解决,首先理解怎么取加两个链表,即链表按照,尾节点往前的顺序每一位相加,如果有进位则在下一个节点相加的时候算上,每一位加和为新链表的一个结点。这看上去跟数学加法一样。所以我们的解题思路为:翻转要相加的两个链表,这样就可以从原链表的尾节点开始相加。
同步遍历两个逆序链表,每一个节点的值相加,通过是要使用变量记录是否进位。
当链表遍历完成后 判断是否还有进位 如果有再添加一个结点,
再次翻转两个链表使其复原,并翻转新链表,则得到的题解。
private Node addLists(Node head1, Node head2) {
head1 = reverseList(head1);
head2 = reverseList(head2);
//进位标识
int ca = 0;
int n1 = 0;
int n2 = 0;
int sum = 0;
Node addHead = new Node(0);
Node dummyHead = addHead;
Node cur1 = head1;
Node cur2 = head2;
while (cur1 != null || cur2 != null) {
n1 = cur1 == null ? 0 : cur1.value;
n2 = cur2 == null ? 0 : cur2.value;
sum = n1 + n2 + ca;
Node node = new Node(sum % 10);
System.out.println( sum % 10);
ca = sum / 10;
dummyHead.next = node;
dummyHead = dummyHead.next;
cur1 = cur1 == null ? null : cur1.next;
cur2 = cur2 == null ? null : cur2.next;
}
if (ca > 0) {
dummyHead.next = new Node(ca);
}
head1 = reverseList(head1);
head2 = reverseList(head2);
addHead = addHead.next;
return reverseList(addHead);
}
private Node reverseList(Node head) {
Node cur = head;
Node pre = null;
Node next = null;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
//注意这里返回的是赋值当前比较元素
return pre;
}
删除有序/无序链表中重复的元素
删除有序链表中的重复元素
删除有序链表中的重复元素比较简单,因为链表本身有序,所以如果元素值重复,那么必定相邻,所以删除重复元素的方法为:
如一个链表为 36 -> 37 -> 65 -> 76 -> 97 -> 98 -> 98 -> 98 -> 98 -> 98 删除重复元素后为: 36 -> 37 -> 65 -> 76 -> 97 -> 98
private void delSortSame(Node head) {
if (head == null || head.next == null) {
return;
}
Node dummy = head;
while (dummy.next != null) {
if (dummy.value == dummy.next.value) {
dummy.next = dummy.next.next;
} else {
dummy = dummy.next;
}
}
}
删除无序链表中的重复元素
删除无序链表中的重复元素,就要求我们必须使用一个指针记住当前考察元素 cur 的上一个元素 pre ,并以此遍历考察元素之后的所有节点,如果有重复则将 pre 指针的 next 指针指向当 cur.next; 重复遍历每个节点,直至链表结尾。
如一个链表删除重复元素前为: 0 -> 0 -> 3 -> 5 -> 3 -> 0 -> 1 -> 4 -> 5 -> 7 删除重复元素后为: 0 -> 3 -> 5 -> 1 -> 4 -> 7
private void delSame(Node head) {
if (head == null || head.next == null) {
return;
}
Node pre = null;
Node next = null;
Node cur = head;
while (cur != null) {
//当前考察的元素的前一个节点
pre = cur;
//当前考察元素
next = cur.next;
//从遍历剩余链表删除重复元素
while (next != null) {
if (cur.value == next.value) {
//删除相同元素
pre.next = next.next;
}else {
//移动指针
pre = next;
}
//移动指针
next = next.next;
}
//考察下一个元素
cur = cur.next;
}
}
重排链表
其实这也是一系列的题目,主要考察了我们对于额外空间复杂度为O(1) 的链表操作。我们先看第一道题:
按照左右半区的方式重新排列组合单链表题目 给定一个单链表L: L0→L1→…→Ln-1→Ln, 重新排列后为 L0→Ln→L1→Ln-1→L2→Ln-2→… 要求必须在不改变节点值的情况下进行原地操作。
我们先来分析一下题目,要想重排链表,必须先找到链表的中间节点,然后分离左右两部链表,然后按左边一个,右边一个的顺序排列链表。我们假设链表为基数的时候, N/2 位置的节点算左半链表, 那么右半链表就会比左半链表多一个节点。当左半链表为最后一个节点的时候我们只需要将剩余的右半链表设为其下一个节点即可。 N 为偶数的时候就好说了,N/2 + 1 为右半链表的开始,重拍最后只需要将左半链表为最后一个节点指向 null,恰巧此时右半链表为 null 所以重拍最后一步就是 left.next = right 下面我们来看题解:
private void relocate1(Node head) {
//如果链表长度小于2 则不需要重新操作
if (head == null || head.next == null) {
return;
}
//使用快慢指针 遍历链表找到链表的中点
Node mid = head;
Node right = head.next;
while (right.next != null && right.next.next != null) {
mid = mid.next;
right = right.next.next;
}
//拆分左右半区链表
right = mid.next;
mid.next = null;
//按要求合并
mergeLR(head, right);
}
private void mergeLR(Node left, Node right) {
Node temp = null;
while (left.next != null) {
temp = right.next;
right.next = left.next;
left.next = right;
//这里每次向后移动两个位置 也就是原来的 left.next
left = right.next;
right = temp;
}
left.next = right;
}
今日头条的一个重排链表题目给定一个链表 1 -> 92 -> 8 -> 86 -> 9 -> 43 -> 20 链表的特征是奇数位升序,偶数位为降序,要求重新排列链表并保持链表整体为升序
这道题和左右半区重排链表类似,其实这可以理解为一个已经进行重排后的链表,现在要执行上一道重排的逆过程。要满足这个条件,我们必须假设偶数位最小的节点大于奇数位最大的元素。我想出题人也是这意思。如果不是的话也不麻烦上边我们也讲了归并排序的方法,只是一次归并而已。下面来看满足数位最小的节点大于奇数位最大的元素的解法:此题考察了面试者对链表的基本操作以及如何翻转一个链表
private Node relocate2(Node head) {
//新建一个左右连个链表的头指针
Node left = new Node();
Node right = new Node();
Node dummyLeft = left;
Node dummyRight = right;
int i = 0;
while (head != null) {
//因为 i 从0 开始 链表的头节点算是奇数位所以 i 先自增 再比较
i++;
if (i % 2 == 0) {
dummyRight.next = head;
dummyRight = dummyRight.next;
} else {
dummyLeft.next = head;
dummyLeft = dummyLeft.next;
}
//每次赋值后记得将下一个节点置位 null
Node next = head.next;
head.next = null;
head = next;
}
right = reverseList(right.next);
dummyLeft.next = right;
return left.next;
}
判断两个单链表(无环)是相交题目: 判断两个无环链表是否相交,如果相交则返回第一个相交节点,如果不想交返回 null 。
我们来分析一下这道题,我们假设两个单链表相交,那从相交的节点开始到结束,一直到两个链表都结束,那么后边这段链表相当于是共享的。我们还可以知道如果将这两个链表的末尾对齐,这两个链表的尾节点一定是相等的,所以我们的解题思路如下:想让一个链表遍历一遍,并记录其长度
在遍历另一个链表,遍历过程中 n 每次自减一
遍历结束后,指针 cur1 指向链表 head1 的最后一个节点,同理指针 cur2 指向 head2 的最后一个节点,如果此时 cur1 != cur2 那么根据题意这两个链表不想交。
遍历结束后,我们假设 hea1 要比 head2 长,那么 n 一定为正数,代表了 head1 头节点指针如果向右移动 n 个数 剩余链表的长度将和 head2 一样长
此后 point1 和 point2 一起走那么这两个 point 指向的节点总会相等,第一次相等的点即为两个链表相交的点。
private Node intersect(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node cur1 = head1;
Node cur2 = head2;
int n = 0;
while (cur1.next != null) {
n++;
cur1 = cur1.next;
}
while (cur2.next != null) {
n--;
cur2 = cur2.next;
}
if (cur1 != cur2) {
return null;
}
//令 cur1 指向 较长的链表,cur2 指向较短的链表
if (n > 0) {
cur1 = head1;
cur2 = head2;
} else {
cur1 = head2;
cur2 = head1;
}
n = Math.abs(n);
//较长的链表先走 n 步
while (n != 0) {
cur1 = cur1.next;
}
//两个链表一起走 第一次相等节点即为相交的第一个节点
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
1. 相交链表
编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表**:**
在节点 c1 开始相交。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
注意:如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
设置快慢指针
public ListNode getIntersectionNode (ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode p1 = headA;
ListNode p2 = headB;
while (p1 != p2) {
if (p1 == null) p1 = headB;
else p1 = p1.next;
if (p2 == null) p2 = headA;
else p2 = p2.next;
}
return p1;
}
2. 反转链表
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶: 你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
递归法:
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode rest = head.next;
ListNode newHead = reverseList(rest);
rest.next = head;
head.next = null;
return newHead;
}
迭代法:
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
3. 合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
双指针思想
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
4. 删除排序链表中的重复元素
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
输入: 1->1->2
输出: 1->2
示例 2:
输入: 1->1->2->3->3
输出: 1->2->3
一次遍历,注意边界条件。
public ListNode deleteDuplicates(ListNode head) {
ListNode cur = head;
while(cur != null && cur.next != null){
if(cur.val == cur.next.val)
cur.next = cur.next.next;
else
cur = cur.next;
}
return head;
}
5. 删除链表的倒数第N个节点
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:
给定的 n 保证是有效的。
进阶:
你能尝试使用一趟扫描实现吗?
设置哑节点1,让它走 n+1 步,再设置哑节点2,然后哑节点1和哑节点2一起移动,直到哑节点1走完链表,此时哑节点1和哑节点2之间正好隔着 n 个节点,再通过哑节点2删除倒数第 n 个节点。
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode first = dummy;
for (int i = 1; i <= n + 1;i++){
first = first.next;
}
ListNode second = dummy;
while(first != null){
first = first.next;
second = second.next;
}
second.next = second.next.next;
return dummy.next;
}
6. 回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶: 你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
设置快慢指针,移动速度分别是2和1。当快指针到达链表尾部的时候,慢指针就在正中间(奇数个节点的情况下)或者正中间的左边(偶数个节点的情况下),再将慢指针向后移动一位,反转以慢指针为头的链表,再逐个节点对比是否相等。
public boolean isPalindrome (ListNode head) {
if (head == null || head.next == null) return true;
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
slow = slow.next;
slow = reverse(slow);
while (slow != null) {
if (head.val == slow.val) {
head = head.next;
slow = slow.next;
} else
return false;
}
return true;
}
private ListNode reverse (ListNode head) {
ListNode newHead = null;
while (head != null) {
ListNode nextNode = head.next;
head.next = newHead;
newHead = head;
head = nextNode;
}
return newHead;
}
7. 两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
设置哑节点,注意循环条件,指针移动的速度是2(因为需要两两交换节点)。
public ListNode swapPairs (ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
while (pre.next != null && pre.next.next != null) {
ListNode l1 = pre.next;
ListNode l2 = pre.next.next;
l1.next = l2.next;
l2.next = l1;
pre.next = l2;
pre = l1;
}
return dummy.next;
}
8. 两数相加 II
给定两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储单个数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
进阶:
如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。
示例:
输入: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出: 7 -> 8 -> 0 -> 7
既然不能改变链表的结构(翻转链表),那就用一个栈来保存链表中的值,可以做到逆向输出。
相加部分的代码逻辑就按常规思路写。
public ListNode addTwoNumbers (ListNode l1, ListNode l2) {
Stack l1Stack = listNodetoStack(l1);
Stack l2Stack = listNodetoStack(l2);
int carry = 0;
ListNode head = new ListNode(-1);
while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carry != 0) {
int x = l1Stack.isEmpty() ? 0 : l1Stack.pop();
int y = l2Stack.isEmpty() ? 0 : l2Stack.pop();
int sum = x + y + carry;
ListNode node = new ListNode(sum % 10);
carry = sum / 10;
node.next = head.next;
head.next = node;
}
return head.next;
}
private Stack listNodetoStack (ListNode head) {
Stack stack = new Stack<>();
while (head != null) {
stack.push(head.val);
head = head.next;
}
return stack;
}
9. 分隔链表
给定一个头结点为 root 的链表, 编写一个函数以将链表分隔为 k 个连续的部分。
每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过 1,也就是说可能有些部分为 null。
这k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度应该大于或等于后面的长度。
返回一个符合上述规则的链表的列表。
举例: 1->2->3->4, k = 5 // 5 结果 [ [1], [2], [3], [4], null ]
示例 1:
输入:
root = [1, 2, 3], k = 5
输出: [[1],[2],[3],[],[]]
解释:
输入输出各部分都应该是链表,而不是数组。
例如, 输入的结点 root 的 val= 1, root.next.val = 2, \root.next.next.val = 3, 且 root.next.next.next = null。
第一个输出 output[0] 是 output[0].val = 1, output[0].next = null。
最后一个元素 output[4] 为 null, 它代表了最后一个部分为空链表。
示例 2:
输入:
root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3
输出: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]
解释:
输入被分成了几个连续的部分,并且每部分的长度相差不超过1.前面部分的长度大于等于后面部分的长度。
提示:root 的长度范围: [0, 1000].
输入的每个节点的大小范围:[0, 999].
k 的取值范围: [1, 50].
先统计出链表长度,除以 k, 求商和余数,其中:余数代表最后结果中有多少个长链表
商代表每个短链表的长度(结果集中后部的链表)
长链表比短链表多一个节点
public ListNode[] splitListToParts (ListNode root, int k) {
ListNode cur = root;
int len = 0;
while (cur != null) {
cur = cur.next;
len++;
}
int mod = len % k;
int size = len / k;
cur = root;
ListNode[] ans = new ListNode[k];
for (int i = 0; cur != null && i < k; i++) {
ans[i] = cur;
int curSize = size + (mod-- > 0 ? 1 : 0);
for (int j = 0; j < curSize - 1; j++) {
cur = cur.next;
}
ListNode next = cur.next;
cur.next = null;
cur = next;
}
return ans;
}
10. 奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:应当保持奇数节点和偶数节点的相对顺序。
链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
设置三个指针,其中奇指针和偶指针是很自然能想到的,evenHead起辅助作用,用于将奇链表和偶链表结合起来。
public ListNode oddEvenList (ListNode head) {
ListNode odd = head;
ListNode even = head.next;
ListNode evenHead = even;
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenHead;
return head;
}