Hello 继上次的 搞懂基本排序算法,这个一星期,我总结了,我所学习和思考的单链表基础知识和常见面试题,这些题有的来自 《剑指 offer》 ,有的来自《程序员代码面试指南》,有的来自 leetCode,不是很全面,但都具备必定表明性,相信你们看完之后必定跟我同样,对面试的时候算法题又多了一份自信。不过文章仍然是又臭又长,但愿你们备好咖啡,火腿肠,方便面之类的,慢慢看,若是我有哪些理解不对的地方,也但愿你们能在评论区为我指出,也算是对我码这么多字的承认吧。java
链表(Linked list)是一种常见的基础数据结构,是一种线性表,可是并不会按线性的顺序存储数据,而是在每个节点里存到下一个节点的指针(Pointer),简单来讲链表并不像数组那样将数组存储在一个连续的内存地址空间里,它们能够不是连续的由于他们每一个节点保存着下一个节点的引用(地址),因此较之数组来讲这是一个优点。node
对于单链表的一个节点咱们常用下边这种代码表示:git
public class Node{
//节点的值
int value;
//指向下一个节点的指针(java 中表现为下一个节点的引用)
Node next;
public void Node(int value){
this.value = value;
}
}
复制代码
单链表的特色程序员
上一节咱们说了什么是单链表,那么咱们都知道一个数组它具备增删改查的基本操做,那么咱们单链表做为一种常见的数据结构类型也是具备这些操做的那么咱们就来看下对于单链表有哪些基本操做:github
因为单链表的存储地址不是连续的,链表并不具备直接获取链表长度的功能,对于一个链表的长度咱们只能一次去遍历链表的节点,直到找到某个节点的下一个节点为空的时候获得链表的总长度,注意这里的出发点并非一个空链表而后依次添加节点后,而后去读取已经记录的节点个数,而是已知一个链表的头结点而后去获取这个链表的长度:面试
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 指针指向已知链表的头节点就能够了。数组
一、 在已有链表头部插入一个节点bash
public Node addAtHead(Node head, int value){
Node newHead = new Node(value);
newHead.next = head;
return newHead;
}
复制代码
二、在已有链表的尾部插入一个节点:数据结构
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;
}
复制代码
三、在指定位置添加一个节点
// 注意这里 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);因此链表相对于数组插入的效率要高一些,删除同理。
因为上边介绍了链表添加元素的方法这里对于链表删除节点的方法不在详细介绍直接给出代码:
一、 删除头部节点 也就是删除索引为 0 的节点:
public Node deleteHead(Node head) throws Exception {
if (head == null) {
throw new Exception("当前链表为空!");
}
return head.next;
}
复制代码
二、 删除尾节点
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;
}
复制代码
三、 删除指定索引的节点:
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-1 步后,而后让慢指针出发。快慢指针每次都只移动一个位置,当快指针移动到链表末尾的时候,慢指针是否就正处于倒数第 N 个节点的位置呢。
是这里把这两个指针称之为快慢指针是不正确的,由于快慢指针是指一个指针移动的快一个指针移动的慢,而此题中 快指针只是比慢指针先移动了 n-1 个位置而已,移动速度是相同的。
若是上边的讲解很差理解,这里提供另一种思路,就是想象一下,上述快慢指针的移动过程,是否就至关于一个固定窗口大小为 n 的滑动窗口:
下面咱们来看下函数实现:
/**
* 注意咱们通常说倒数第 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 + 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)
翻转单链表是我感受比较难的基础题,那么先来屡一下思路:一个节点包含指向下一节点的引用,翻转的意思就是对要原来指向下一个节点引用指向上一个节点
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 节点下一个节点。
下面咱们开看代码(你可能有更简便的解法,省去几个变量,可是下面的解法应该是最好理解的);
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 。
咱们来分析一下这道题,咱们假设两个单链表相交,那从相交的节点开始到结束,一直到两个链表都结束,那么后边这段链表至关因而共享的。咱们还能够知道若是将这两个链表的末尾对齐,这两个链表的尾节点必定是相等的,因此咱们的解题思路以下:
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;
}
复制代码
上篇文章搞懂排序算法评论有人说,文章太长了。没想到这篇文章写着写着又这么长了。还请你们耐下心来看,每到题本身耐下心来作一遍。等你们都搞懂之后,相信你们也就差很少无所畏惧单链表的面试题了。
欢迎你们关注个人我的博客地址,本文算法题也上传到个人 github上了。NodePractice 后续我将开始学习数组,和字符串的算法题。相信不久未来又能见到个人又臭又长的文章了。
最后 愿天不负有心人。