mirror of
https://github.com/chefyuan/algorithm-base.git
synced 2024-11-28 06:48:53 +00:00
test
This commit is contained in:
parent
b81ee61e58
commit
dfdec0d511
125
gif-algorithm/链表篇/234. 回文链表.md
Normal file
125
gif-algorithm/链表篇/234. 回文链表.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#### leetcode 234. 回文链表
|
||||||
|
|
||||||
|
请判断一个链表是否为回文链表。
|
||||||
|
|
||||||
|
示例 1:
|
||||||
|
|
||||||
|
```
|
||||||
|
输入: 1->2
|
||||||
|
输出: false
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
示例 2:
|
||||||
|
|
||||||
|
```java
|
||||||
|
输入: 1->2->2->1
|
||||||
|
输出: true
|
||||||
|
```
|
||||||
|
|
||||||
|
题目解析:
|
||||||
|
|
||||||
|
题目理解起来很简单,判断是否为回文,如果单纯判断一个字符串或者数组是不是回文很容易。因为数组查询元素的时间复杂度为O(1),但是链表的查询时间复杂度为O(n),而且题目中的链表为单链表,指针只能后移不能前移。所以我们判断起来会比较困难。
|
||||||
|
|
||||||
|
巧用数组法:
|
||||||
|
|
||||||
|
我们首先将链表的所有元素都保存在数组中,然后再利用双指针遍历数组,进而来判断是否为回文。这个方法很容易理解,而且代码实现也比较简单。
|
||||||
|
|
||||||
|
```java
|
||||||
|
class Solution {
|
||||||
|
public boolean isPalindrome(ListNode head) {
|
||||||
|
//这里需要用动态数组,因为我们不知道链表的长度
|
||||||
|
List<Integer> arr = new ArrayList<Integer>();
|
||||||
|
ListNode copynode = head;
|
||||||
|
//将链表的值复制到数组中
|
||||||
|
while (copynode != null) {
|
||||||
|
arr.add(copynode.val);
|
||||||
|
copynode = copynode.next;
|
||||||
|
}
|
||||||
|
// 双指针遍历数组
|
||||||
|
int back = 0;
|
||||||
|
int pro = arr.size() - 1;
|
||||||
|
while (back < pro) {
|
||||||
|
//判断两个指针的值是否相等
|
||||||
|
if (!arr.get(pro).equals(arr.get(back))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//移动指针
|
||||||
|
back++;
|
||||||
|
pro--;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
这个方法可以直接通过,但是这个方法需要辅助数组,那我们还有其他更好的方法吗?
|
||||||
|
|
||||||
|
双指针翻转链表法
|
||||||
|
|
||||||
|
在上个题目中我们知道了如何找到链表的中间节点,那我们可以在找到中间节点之后,对后半部分进行翻转,翻转之后,重新遍历前半部分和后半部分进行判断是否为回文。
|
||||||
|
|
||||||
|
动图解析:
|
||||||
|
|
||||||
|
![翻转链表部分](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/翻转链表部分.1v2ncl72ligw.gif)
|
||||||
|
|
||||||
|
```java
|
||||||
|
class Solution {
|
||||||
|
public boolean isPalindrome(ListNode head) {
|
||||||
|
if (head==null || head.next==null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
//找到中间节点,也就是翻转的头节点,这个在昨天的题目中讲到
|
||||||
|
//但是今天和昨天有一些不一样的地方就是,如果有两个中间节点返回第一个,昨天的题目是第二个
|
||||||
|
ListNode midenode = searchmidnode(head);
|
||||||
|
//原地翻转链表,需要两个辅助指针。这个也是面试题目,大家可以做一下
|
||||||
|
//这里我们用的是midnode.next需要注意,因为我们找到的是中点,但是我们翻转的是后半部分
|
||||||
|
|
||||||
|
ListNode backhalf = reverse(midenode.next);
|
||||||
|
//遍历两部分链表,判断值是否相等
|
||||||
|
ListNode p1 = head;
|
||||||
|
ListNode p2 = backhalf;
|
||||||
|
while (p2 != null) {
|
||||||
|
if (p1.val != p2.val) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p1 = p1.next;
|
||||||
|
p2 = p2.next;
|
||||||
|
}
|
||||||
|
// 还原链表并返回结果,这一步是需要注意的,我们不可以破坏初始结构,我们只是判断是否为回文,
|
||||||
|
//当然如果没有这一步也是可以AC,但是面试的时候题目要求可能会有这一条。
|
||||||
|
midenode.next = reverse(backhalf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
//找到中间的部分
|
||||||
|
public ListNode searchmidnode (ListNode head) {
|
||||||
|
ListNode fast = new ListNode(-1);
|
||||||
|
ListNode slow = new ListNode(-1);
|
||||||
|
fast = head;
|
||||||
|
slow = head;
|
||||||
|
//找到中点
|
||||||
|
while (fast.next != null && fast.next.next != null) {
|
||||||
|
fast = fast.next.next;
|
||||||
|
slow = slow.next;
|
||||||
|
}
|
||||||
|
return slow;
|
||||||
|
}
|
||||||
|
//翻转链表
|
||||||
|
public ListNode reverse (ListNode slow) {
|
||||||
|
ListNode low = null;
|
||||||
|
ListNode temp = null;
|
||||||
|
//翻转链表
|
||||||
|
while (slow != null) {
|
||||||
|
temp = slow.next;
|
||||||
|
slow.next = low;
|
||||||
|
low = slow;
|
||||||
|
slow = temp;
|
||||||
|
}
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
149
gif-algorithm/链表篇/leetcode142环形链表2.md
Normal file
149
gif-algorithm/链表篇/leetcode142环形链表2.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
#### leetcode 142. 环形链表 II
|
||||||
|
|
||||||
|
题目描述:
|
||||||
|
|
||||||
|
今天给大家介绍比较有特点的题目,也是一个特别经典的题目,判断链表中有没有环,并返回环的入口。
|
||||||
|
|
||||||
|
我们可以先做一下这个题目,就是如何判断链表中是否有环呢?下图则为链表存在环的情况。
|
||||||
|
|
||||||
|
![image-20201027175552961](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/image-20201027175552961.63sz69rbes00.png)
|
||||||
|
|
||||||
|
判断链表是否有环是很简单的一个问题,我们只需利用之前的快慢指针即可,大家想一下,指针只要进入环内就只能在环中循环,那么我们可以利用快慢指针,虽然慢指针的速度小于快指针但是,总会进入环中,当两个指针都处于环中时,因为移动速度不同,两个指针必会相遇。
|
||||||
|
|
||||||
|
我们可以这样假设,两个孩子在操场顺时针跑步,一个跑的快,一个跑的慢,跑的快的那个孩子总会追上跑的慢的孩子。
|
||||||
|
|
||||||
|
环形链表:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Solution {
|
||||||
|
public boolean hasCycle(ListNode head) {
|
||||||
|
//特殊情况,无节点或只有一个节点的情况
|
||||||
|
if(head == null || head.next == null){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//设置快慢指针
|
||||||
|
ListNode pro = head.next;
|
||||||
|
ListNode last = head;
|
||||||
|
//循环条件
|
||||||
|
while( pro != null && pro.next!=null){
|
||||||
|
pro=pro.next.next;
|
||||||
|
last=last.next;
|
||||||
|
//两指针相遇
|
||||||
|
if(pro == last){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//循环结束,指针没有相遇,说明没有环。相当于快指针遍历了一遍链表
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
判断链表是不是含有环很简单,但是我们想找到环的入口可能就没有那么容易了。(入口则为下图绿色节点)
|
||||||
|
|
||||||
|
然后我们返回的则为绿色节点的索引,则返回2。
|
||||||
|
|
||||||
|
![image-20201027180921770](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/image-20201027180921770.21fh8pt3cuv4.png)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### HashSet
|
||||||
|
|
||||||
|
我们可以利用HashSet来做,之前的文章说过HashSet是一个不允许有重复元素的集合。所以我们通过HashSet来保存链表节点,对链表进行遍历,如果链表不存在环则每个节点都会被存入环中,但是当链表中存在环时,则会发重复存储链表节点的情况,所以当我们发现HashSet中含有某节点时说明该节点为环的入口,返回即可。
|
||||||
|
|
||||||
|
下图中,存储顺序为 0,1,2,3,4,5,6,**2 **因为2已经存在,则返回。
|
||||||
|
|
||||||
|
![image-20201027182649669](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/image-20201027182649669.2g8gq4ik6xs0.png)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Solution {
|
||||||
|
public ListNode detectCycle(ListNode head) {
|
||||||
|
|
||||||
|
if (head == null) {
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
if (head.next == null) {
|
||||||
|
return head.next;
|
||||||
|
}
|
||||||
|
//创建新的HashSet,用于保存节点
|
||||||
|
HashSet<ListNode> hash = new HashSet<ListNode>();
|
||||||
|
//遍历链表
|
||||||
|
while (head != null) {
|
||||||
|
//判断哈希表中是否含有某节点,没有则保存,含有则返回该节点
|
||||||
|
if (hash.contains(head)) {
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
//不含有,则进行保存,并移动指针
|
||||||
|
hash.add(head);
|
||||||
|
head = head.next;
|
||||||
|
}
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 快慢指针
|
||||||
|
|
||||||
|
这个方法是比较巧妙的方法,但是不容易想到,也不太容易理解,利用快慢指针判断是否有环很容易,但是判断环的入口就没有那么容易,之前说过快慢指针肯定会在环内相遇,见下图。
|
||||||
|
|
||||||
|
![image-20201027184755943](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/image-20201027184755943.3edot8s2xi60.png)
|
||||||
|
|
||||||
|
上图黄色节点为快慢指针相遇的节点,此时
|
||||||
|
|
||||||
|
快指针走的距离:**a+(b+c)n+b**
|
||||||
|
|
||||||
|
很容易理解b+c为环的长度,a为直线距离,b为绕了n圈之后又走了一段距离才相遇,所以相遇时走的总路程为a+(b+c)n+b,合并同类项得a+(n+1)b+nc。
|
||||||
|
|
||||||
|
慢指针走的距离:**a+(b+c)m+b**,m代表圈数。
|
||||||
|
|
||||||
|
然后我们设快指针得速度是慢指针的2倍,含义为相同时间内,快指针走过的距离是慢指针的2倍。
|
||||||
|
|
||||||
|
**a+(n+1)b+nc=2[a+(m+1)b+mc]**整理得**a+b=(n-2m)(b+c),**那么我们可以从这个等式上面发现什么呢?**b+c**
|
||||||
|
|
||||||
|
为一圈的长度。也就是说a+b等于n-2m个环的长度。为了便于理解我们看一种特殊情况,当n-2m等于1,那么a+b=b+c整理得,a=c此时我们只需重新释放两个指针,一个从head释放,一个从相遇点释放,速度相同,因为a=c所以他俩必会在环入口处相遇,则求得入口节点索引。
|
||||||
|
|
||||||
|
算法流程:
|
||||||
|
|
||||||
|
1.设置快慢指针,快指针速度为慢指针的2倍
|
||||||
|
|
||||||
|
2.找出相遇点
|
||||||
|
|
||||||
|
3.在head处和相遇点同时释放相同速度且速度为1的指针,两指针必会在环入口处相遇
|
||||||
|
|
||||||
|
![环形链表2](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/环形链表2.elwu1pw2lw0.gif)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Solution {
|
||||||
|
public ListNode detectCycle(ListNode head) {
|
||||||
|
//快慢指针
|
||||||
|
ListNode fast = head;
|
||||||
|
ListNode low = head;
|
||||||
|
//设置循环条件
|
||||||
|
while (fast != null && fast.next != null) {
|
||||||
|
fast = fast.next.next;
|
||||||
|
low = low.next;
|
||||||
|
//相遇
|
||||||
|
if (fast == low) {
|
||||||
|
//设置一个新的指针,从头节点出发,慢指针速度为1,所以可以使用慢指针从相遇点出发
|
||||||
|
ListNode newnode = head;
|
||||||
|
while (newnode != low) {
|
||||||
|
low = low.next;
|
||||||
|
newnode = newnode.next;
|
||||||
|
}
|
||||||
|
//在环入口相遇
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
64
gif-algorithm/链表篇/leetcode82删除排序链表中的重复元素II.md
Normal file
64
gif-algorithm/链表篇/leetcode82删除排序链表中的重复元素II.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#### 82. 删除排序链表中的重复元素 II
|
||||||
|
|
||||||
|
题目描述
|
||||||
|
|
||||||
|
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中没有重复出现的数字。
|
||||||
|
|
||||||
|
示例 1:
|
||||||
|
|
||||||
|
```java
|
||||||
|
输入: 1->2->3->3->4->4->5
|
||||||
|
输出: 1->2->5
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
示例 2:
|
||||||
|
|
||||||
|
```java
|
||||||
|
输入: 1->1->1->2->3
|
||||||
|
输出: 2->3
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意这里会将重复的值全部删除,1,1,2,3最后只会保留2,3。
|
||||||
|
|
||||||
|
这道题目还是很简单的,更多的是考察大家的代码完整性,删除节点也是题库中的一类题目,我们可以可以通过这个题目举一反三。去完成其他删除阶段的题目。
|
||||||
|
|
||||||
|
链表的题目建议大家能有指针实现还是尽量用指针实现,很多链表题目都可以利用辅助空间实现,我们也可以用,学会了那种方法的同时应该再想一下可不可以利用指针来完成。下面我们来思考一下这个题目如何用指针实现吧!
|
||||||
|
|
||||||
|
做题思路:
|
||||||
|
|
||||||
|
这个题目也是利用我们的双指针思想,一个走在前面,一个在后面紧跟,前面的指针就好比是侦察兵,当发现重复节点时,后面指针停止移动,侦察兵继续移动,直到移动完重复节点,然后将该节点赋值给后节点。思路是不是很简单啊,那么我们来看一下动图模拟吧。
|
||||||
|
|
||||||
|
注:这里为了表达更直观,所以仅显示了该链表中存在的节点
|
||||||
|
|
||||||
|
![删除重复节点2](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/删除重复节点2.3btmii5cgxa0.gif)
|
||||||
|
|
||||||
|
```java
|
||||||
|
class Solution {
|
||||||
|
public ListNode deleteDuplicates(ListNode head) {
|
||||||
|
if(head == null||head.next==null){
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
ListNode pre = head;
|
||||||
|
ListNode low = new ListNode(0);
|
||||||
|
low.next = pre;
|
||||||
|
ListNode ret = new ListNode(-1);
|
||||||
|
ret = low;
|
||||||
|
while(pre != null && pre.next != null) {
|
||||||
|
if (pre.val == pre.next.val) {
|
||||||
|
while (pre != null && pre.next != null && pre.val == pre.next.val) {
|
||||||
|
pre = pre.next;
|
||||||
|
}
|
||||||
|
pre = pre.next;
|
||||||
|
low.next = pre;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
pre = pre.next;
|
||||||
|
low = low.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
151
gif-algorithm/链表篇/关于链表的那些事.md
Normal file
151
gif-algorithm/链表篇/关于链表的那些事.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# 链表详解
|
||||||
|
|
||||||
|
阅读完本文你会有以下收获
|
||||||
|
|
||||||
|
1.知道什么是链表?
|
||||||
|
|
||||||
|
2.了解链表的几种类型。
|
||||||
|
|
||||||
|
3.了解链表如何构造。
|
||||||
|
|
||||||
|
4.链表的存储方式
|
||||||
|
|
||||||
|
5.如何遍历链表
|
||||||
|
|
||||||
|
6.了解链表的操作。
|
||||||
|
|
||||||
|
7.知道链表和数组的不同点
|
||||||
|
|
||||||
|
8.掌握链表的经典题目。
|
||||||
|
|
||||||
|
### 链表的定义:
|
||||||
|
|
||||||
|
> 定义:链表是一种递归的数据结构,他或者为空(null),或者是指向一个结点(node)的引用,该结点含有一个泛型的元素和一个指向另一条链表的引用。
|
||||||
|
|
||||||
|
我们来对其解读一下,链表是一种常见且基础的数据结构,是一种线性表,但是他不是按线性顺序存取数据,而是在每一个节点里存到下一个节点的地址。我们可以这样理解,链表是通过指针串联在一起的线性结构,每一个链表结点由两部分组成,数据域及指针域,链表的最后一个结点指向null。也就是我们所说的空指针。
|
||||||
|
|
||||||
|
### 链表的几种类型
|
||||||
|
|
||||||
|
我们先来看一下链表的可视化表示方法,以便更好的对其理解。
|
||||||
|
|
||||||
|
- 用长方形表示对象
|
||||||
|
- 将实例变量的值写在长方形中;
|
||||||
|
- 用指向被引用对象的箭头表示引用关系。
|
||||||
|
|
||||||
|
#### 单链表
|
||||||
|
|
||||||
|
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。
|
||||||
|
|
||||||
|
我们通过上面说到的可视化表示方法,构造单链表的可视化模型,如图所示。
|
||||||
|
|
||||||
|
![image-20201101143220993](E:\Typora笔记\CSDN\leetcode通关笔记\静态图\单链表.png)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 双向链表
|
||||||
|
|
||||||
|
上面提到了单链表的节点只能指向节点的下一个节点。而双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接,所以双链表既能向前查询也可以向后查询。
|
||||||
|
|
||||||
|
![双链表](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/双链表.3cw4hra1g3q0.png)
|
||||||
|
|
||||||
|
####
|
||||||
|
|
||||||
|
还有一个常用的链表则为循环单链表,则单链表尾部的指针指向头节点。例如在leetcode61旋转链表中,我们就是先将链表闭合成环,找到新的打开位置,并定义新的表头和表尾。
|
||||||
|
|
||||||
|
### 构造链表
|
||||||
|
|
||||||
|
java是面向对象语言,实现链表很容易。我们首先用一个嵌套类来定义节点的抽象数据类型
|
||||||
|
|
||||||
|
```java
|
||||||
|
private class Node {
|
||||||
|
Item item;
|
||||||
|
Node next;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
现在我们需要构造一条含有one,two,three的链表,我们首先为每个元素创造一个节点
|
||||||
|
|
||||||
|
```java
|
||||||
|
Node first = new Node();
|
||||||
|
Node second = new Node();
|
||||||
|
Node third = new Node();
|
||||||
|
```
|
||||||
|
|
||||||
|
将每个节点的item域设为所需的值
|
||||||
|
|
||||||
|
```java
|
||||||
|
first.item = "one";
|
||||||
|
second.item = "two";
|
||||||
|
third.item = "three";
|
||||||
|
```
|
||||||
|
|
||||||
|
然后我们设置next域来构造链表
|
||||||
|
|
||||||
|
```java
|
||||||
|
first.next = second;
|
||||||
|
second.next = third;
|
||||||
|
```
|
||||||
|
|
||||||
|
注:此时third的next仍为null,即被初始化的值。
|
||||||
|
|
||||||
|
### 链表的存储方式
|
||||||
|
|
||||||
|
我们知道了如何构造链表,我们再来说一下链表的存储方式。
|
||||||
|
|
||||||
|
我们都知道数组在内存中是连续分布的,但是链表在内存不是连续分配的。链表是通过指针域的指针链接内存中的各个节点。
|
||||||
|
|
||||||
|
所以链表在内存中是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。我们可以根据下图来进行理解。
|
||||||
|
|
||||||
|
![image-20201101153659912](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/image-20201101153659912.9neaap4ogtc.png)
|
||||||
|
|
||||||
|
### 遍历链表
|
||||||
|
|
||||||
|
链表的遍历我们通常使用while循环(for循环也可以但是代码不够简洁)下面我们来看一下链表的遍历代码
|
||||||
|
|
||||||
|
for:
|
||||||
|
|
||||||
|
```java
|
||||||
|
for (Node x = first; x != null; x = x.next) {
|
||||||
|
//处理x.item
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
while:
|
||||||
|
|
||||||
|
```
|
||||||
|
Node x = first;
|
||||||
|
while (x!=null) {
|
||||||
|
//处理x.item
|
||||||
|
x = x.next;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 链表的几种操作
|
||||||
|
|
||||||
|
#### 添加节点
|
||||||
|
|
||||||
|
![image-20201101155937520](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/image-20201101155937520.my13cevp2cg.png)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 删除节点
|
||||||
|
|
||||||
|
删除B节点,如图所示
|
||||||
|
|
||||||
|
![image-20201101155003257](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/image-20201101155003257.4onlntrwj2i0.png)
|
||||||
|
|
||||||
|
我们只需将A节点的next指针指向C节点即可。
|
||||||
|
|
||||||
|
有的同学可能会有这种疑问,B节点这样不会留着内存里吗?java含有自己的内存回收机制,不用自己手动释放内存了,但是C++,则需要手动释放。
|
||||||
|
|
||||||
|
我们通过上图的删除和插入都是O(1)操作。
|
||||||
|
|
||||||
|
链表和数组的比较
|
||||||
|
|
||||||
|
| | 插入/删除操作(时间复杂度) | 查询(时间复杂度) | 存储方式 |
|
||||||
|
| ---- | ------------------------- | ------------------ | ------------ |
|
||||||
|
| 数组 | O(n) | O(1) | 内存连续分布 |
|
||||||
|
| 链表 | O(1) | O(n) | 内存散乱分布 |
|
||||||
|
|
||||||
|
|
||||||
|
|
45
gif-algorithm/链表篇/剑指Offer25合并两个排序的链表.md
Normal file
45
gif-algorithm/链表篇/剑指Offer25合并两个排序的链表.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#### 剑指offer25合并两个有序链表
|
||||||
|
|
||||||
|
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
输入:1->2->4, 1->3->4
|
||||||
|
输出:1->1->2->3->4->4
|
||||||
|
```
|
||||||
|
|
||||||
|
今天的题目思路很简单,但是一遍AC也是不容易的。链表大部分题目考察的都是考生代码的完整性和鲁棒性,所以有些题目我们看着思路很简单,但是想直接通过还是需要下一翻工夫的,所以建议大家将所有链表的题目都自己写一下。实在没有时间做的同学,可以自己在脑子里打一遍代码,想清没一行代码的作用。
|
||||||
|
|
||||||
|
迭代法:
|
||||||
|
|
||||||
|
因为我们有两个升序链表,我们需要将其合并,那么我们需要创建一个新节点headpre,然后我们利用双指针思想,每个链表放置一个指针,然后进行遍历并对比当前指针指向的值。然后headpre.next指向较小值的那个节点,不断迭代,直至到达某一有序链表底部,此时一个链表遍历完成,然后我们将未完全遍历的链表接在我们接在合并链表之后即可。
|
||||||
|
|
||||||
|
这是我们迭代做法,另外这个题目还有一个递归方法,目前先不写,等链表掌握差不多的时候会单独写一篇关于递归的文章,也算是为树的题目做铺垫。
|
||||||
|
|
||||||
|
动图讲解:
|
||||||
|
|
||||||
|
![合并数组](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/合并数组.216f4nn4lti8.gif)
|
||||||
|
|
||||||
|
```java
|
||||||
|
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
|
||||||
|
ListNode headpro = new ListNode(-1);
|
||||||
|
ListNode headtemp = headpro;
|
||||||
|
while (l1 != null && l2 != null) {
|
||||||
|
//接上大的那个
|
||||||
|
if (l1.val >= l2.val) {
|
||||||
|
headpro.next = l2;
|
||||||
|
l2 = l2.next;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
headpro.next = l1;
|
||||||
|
l1 = l1.next;
|
||||||
|
}
|
||||||
|
headpro = headpro.next;
|
||||||
|
}
|
||||||
|
headpro.next = l1 != null ? l1:l2;
|
||||||
|
return headtemp.next;
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
86
gif-algorithm/链表篇/剑指Offer52两个链表的第一个公共节点.md
Normal file
86
gif-algorithm/链表篇/剑指Offer52两个链表的第一个公共节点.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#### 剑指 Offer 52. 两个链表的第一个公共节点
|
||||||
|
|
||||||
|
### 前言
|
||||||
|
|
||||||
|
今天给大家带来一个不是那么难的题目,这个题目的解答方法很多,只要能AC的就是好方法,虽然题目不是特别难但是也是剑指offer上的经典题目所以大家要记得打卡呀。
|
||||||
|
|
||||||
|
然后今天我们的链表板块就算结束啦。周末的时候我会对链表的题目做一个总结,俗话说温故而知新嘛。好啦废话不多说,我们一起来看一下今天的题目吧
|
||||||
|
|
||||||
|
题目描述:
|
||||||
|
|
||||||
|
输入两个链表,找出它们的第一个公共节点。如下图,返回黄色结点即可。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
![image-20201029215837844](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/image-20201029215837844.7ezoerpghyk0.png)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
题目表达是不是也很简单,这个题目我的方法一共有两个,一种就是用HashSet进行存储,一种就是利用双指针,大家有更好的可以在下面讨论呀。
|
||||||
|
|
||||||
|
### HashSet
|
||||||
|
|
||||||
|
这个方法是比较简单的,主要思路就是,先遍历一个链表将链表的所有值都存到哈希表中,然后再遍历另一个链表,如果发现某个结点在哈希表中已经存在那我们直接返回该节点即可,代码也很简单。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Solution {
|
||||||
|
public ListNode getIntersectionNode (ListNode headA, ListNode headB) {
|
||||||
|
ListNode tempa = headA;
|
||||||
|
ListNode tempb = headB;
|
||||||
|
//定义hash表
|
||||||
|
HashSet<ListNode> arr = new HashSet<ListNode>();
|
||||||
|
while (tempa != null) {
|
||||||
|
arr.add(tempa);
|
||||||
|
tempa = tempa.next;
|
||||||
|
}
|
||||||
|
while (tempb != null) {
|
||||||
|
if (arr.contains(tempb)) {
|
||||||
|
return tempb;
|
||||||
|
}
|
||||||
|
tempb = tempb.next;
|
||||||
|
}
|
||||||
|
return tempb;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
下面这个方法比较巧妙,不是特别容易想到,大家可以自己实现一下,这个方法也是利用我们的双指针思想。
|
||||||
|
|
||||||
|
下面我们直接看动图吧,特别直观,一下就可以搞懂。
|
||||||
|
|
||||||
|
![第一次相交的点](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/第一次相交的点.5nbxf5t3hgk0.gif)
|
||||||
|
|
||||||
|
是不是一下就懂了呀,我们利用双指针,当某一指针遍历完链表之后,然后掉头去另一个链表的头部,继续遍历。因为速度相同所以他们第二次遍历的时候肯定会相遇,是不是很浪漫啊!
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Solution {
|
||||||
|
public ListNode getIntersectionNode (ListNode headA, ListNode headB) {
|
||||||
|
//定义两个节点
|
||||||
|
ListNode tempa = headA;
|
||||||
|
ListNode tempb = headB;
|
||||||
|
//循环
|
||||||
|
while (tempa != tempb) {
|
||||||
|
//如果不为空就指针下移,为空就跳到另一链表的头部
|
||||||
|
tempa = tempa!=null ? tempa.next:headB;
|
||||||
|
tempb = tempb!=null ? tempb.next:headA;
|
||||||
|
}
|
||||||
|
return tempa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
好啦,链表的题目就结束啦,希望大家能有所收获,下周就要更新新的题型啦,继续坚持,肯定会有收获的。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
54
gif-algorithm/链表篇/剑指offer2倒数第k个节点.md
Normal file
54
gif-algorithm/链表篇/剑指offer2倒数第k个节点.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# 题目描述:链表中倒数第k个节点
|
||||||
|
|
||||||
|
题目:
|
||||||
|
|
||||||
|
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
|
||||||
|
|
||||||
|
题目分析:
|
||||||
|
|
||||||
|
自己思考一下
|
||||||
|
|
||||||
|
我们遇到这个题目,可能会有什么答题思路呢?
|
||||||
|
|
||||||
|
你看我说的对不对,是不是会想到先遍历一遍链表知道 链表节点的个数,然后再计算出倒数第n个节点。
|
||||||
|
|
||||||
|
比如链表长度为10,倒数第3个节点,不就是正数第8个节点呀,这种方法当然可以啦,是可以实现的,那么我们再思考一下有没有其他方法呢?哦,对,我们可以将链表元素保存到数组里面,然后直接就可以知道倒数第K个节点了。这个方法确实比刚才那个方法省时间了,但是所耗的空间更多了,那我们还有什么方法吗?
|
||||||
|
|
||||||
|
我们可以继续利用我们的双指针呀,但是我们应该怎么做呢?
|
||||||
|
|
||||||
|
双指针法:
|
||||||
|
|
||||||
|
首先一个指针移动K-1位(这里可以根据你的初始化指针决定),然后另一个指针开始启动,他俩移动速度一样,所以他俩始终相差K-1位,当第一个指针到达链表尾部时,第二个指针的指向则为倒数第K个节点。
|
||||||
|
|
||||||
|
![倒数k个节点](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/倒数k个节点.3uh5e9jvbwc0.gif)
|
||||||
|
|
||||||
|
感觉这个方法既巧妙又简单,大家可以自己动手打一下,这个题目是经典题目。
|
||||||
|
|
||||||
|
```java
|
||||||
|
class Solution {
|
||||||
|
public ListNode getKthFromEnd(ListNode head, int k) {
|
||||||
|
//特殊情况
|
||||||
|
if(head==null){
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
//初始化两个指针
|
||||||
|
ListNode pro = new ListNode(-1);
|
||||||
|
ListNode after = new ListNode(-1);
|
||||||
|
//定义指针指向
|
||||||
|
pro = head;
|
||||||
|
after = head;
|
||||||
|
//先移动绿指针到指定位置
|
||||||
|
for(int i = 0; i < k-1; i++) {
|
||||||
|
pro=pro.next;
|
||||||
|
}
|
||||||
|
//两个指针同时移动
|
||||||
|
while (pro.next != null) {
|
||||||
|
pro = pro.next;
|
||||||
|
after = after.next;
|
||||||
|
}
|
||||||
|
//返回倒数第k个节点
|
||||||
|
return after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
60
gif-algorithm/链表篇/面试题 02.03. 删除中间节点.md
Normal file
60
gif-algorithm/链表篇/面试题 02.03. 删除中间节点.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#### 面试题 02.03. 删除中间节点
|
||||||
|
|
||||||
|
给定一个头结点为 head的非空单链表,返回链表的中间结点。
|
||||||
|
|
||||||
|
如果有两个中间结点,则返回第二个中间结点。
|
||||||
|
|
||||||
|
**示例 1:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
输入:[1,2,3,4,5]
|
||||||
|
输出:3
|
||||||
|
```
|
||||||
|
|
||||||
|
> 说明:因为只有一个中间节点
|
||||||
|
|
||||||
|
**示例 2:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
输入:[1,2,3,4,5,6]
|
||||||
|
输出:4
|
||||||
|
```
|
||||||
|
|
||||||
|
> 说明:有两个中间节点所以返回后面那个
|
||||||
|
|
||||||
|
## 题目解析:
|
||||||
|
|
||||||
|
又精心筛选了一个题目,本来想写一下删除节点的题目,然后发现这个题目更符合目前的节奏,所以先写一下这个题目,明天再给大家写删除节点的题目。
|
||||||
|
|
||||||
|
大家先不要看我的题解,先自己想一下怎么做。这个这个题目是想让我们找出中间节点,昨天的题目是让我们倒数第K个节点,想一下这两个题目有什么联系呢?
|
||||||
|
|
||||||
|
先说一下刚开始刷题的小伙伴可能会想到的题解,两次遍历链表,第一次遍历获取链表长度,第二次遍历获取中间链表。
|
||||||
|
|
||||||
|
这个方法很OK,利用数组先将所有链表元素存入数组里,然后再直接获得中间节点。这个也很OK,那么我们有没有一次遍历,且不开辟辅助空间的方法呢?
|
||||||
|
|
||||||
|
昨天的题目是一前一后双指针,两个指针之间始终相差k-1位,我们今天也利用一下双指针的做法吧。
|
||||||
|
|
||||||
|
这种类型的双指针是我们做链表的题目经常用到的,叫做快慢指针。
|
||||||
|
|
||||||
|
一个指针走的快,一个指针走的慢,这个题目我们可以让快指针一次走两步,慢指针一次走一步,当快指针到达链表尾部的时候,慢指针不就到达中间节点了吗?
|
||||||
|
|
||||||
|
链表中节点的个数有可能为奇数也有可能为偶数,这是两种情况,但是我们输出是相同的,那就是输出slow指针指向的节点
|
||||||
|
|
||||||
|
![中间节点](E:\Typora笔记\CSDN\leetcode通关笔记\博客动图\中间节点.gif)
|
||||||
|
|
||||||
|
```java
|
||||||
|
class Solution {
|
||||||
|
public ListNode middleNode(ListNode head) {
|
||||||
|
ListNode fast = head;//快指针
|
||||||
|
ListNode slow = head;//慢指针
|
||||||
|
//循环条件,思考一下跳出循环的情况
|
||||||
|
while (fast!=null && fast.next != null) {
|
||||||
|
fast = fast.next.next;
|
||||||
|
slow = slow.next;
|
||||||
|
}
|
||||||
|
//返回slow指针指向的节点
|
||||||
|
return slow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
104
gif-algorithm/链表篇/面试题 02.05. 链表求和.md
Normal file
104
gif-algorithm/链表篇/面试题 02.05. 链表求和.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#### 面试题 02.05. 链表求和
|
||||||
|
|
||||||
|
之前我们一起做了链表中的几个经典题型,找到倒数第k个节点,找链表中点,判断链表中环的起点,合并链表,反转链表,删除链表中重复值。这些是链表中的经典问题,面试中也经常会考的问题,然后下面我们继续做一道链表题目,也是面试中经常会考的题目,链表求和问题。
|
||||||
|
|
||||||
|
另外有一些小伙伴说,虽然一天一道题不算多,但是每天读题,做题加消化稍微有点跟不上,所以我打算每个周的工作日进行更新题目,到周末的时候对本周的题目进行总结,然后为大家再写一些别的东西。下面我们一起来看一下今天的题目吧。
|
||||||
|
|
||||||
|
> 为保证严谨性,所有文章中的代码都在网站进行验证,大家可以放心食用。
|
||||||
|
|
||||||
|
题目描述:
|
||||||
|
|
||||||
|
给定两个用链表表示的整数,每个节点包含一个数位。
|
||||||
|
|
||||||
|
这些数位是反向存放的,也就是个位排在链表首部。
|
||||||
|
|
||||||
|
编写函数对这两个整数求和,并用链表形式返回结果。
|
||||||
|
|
||||||
|
示例1:
|
||||||
|
|
||||||
|
```java
|
||||||
|
输入:(7 -> 1 -> 6) + (5 -> 9 -> 2),即617 + 295
|
||||||
|
输出:2 -> 1 -> 9,即912
|
||||||
|
```
|
||||||
|
|
||||||
|
示例2:
|
||||||
|
|
||||||
|
```java
|
||||||
|
输入:(9 -> 9) + (9 -> 9),即99+99
|
||||||
|
输出:8->9->1
|
||||||
|
```
|
||||||
|
|
||||||
|
示例3:
|
||||||
|
|
||||||
|
```java
|
||||||
|
输入:(5)+(5),即5+5
|
||||||
|
输出:0->1
|
||||||
|
```
|
||||||
|
|
||||||
|
**题目解析:**
|
||||||
|
|
||||||
|
这个题目很容易理解,就是将链表数值进行求和,刚开始做题的同学可能会有这种思路,这个题目我们分别遍历两个链表得到他们的数,然后进行相加,再放到新的链表中,但是这样是行不通的,
|
||||||
|
|
||||||
|
因为我们需要考虑溢出的情况,java 中 int 型的范围为 -2147483648 到 +2147483648,即 -2^31 到 2^31。
|
||||||
|
|
||||||
|
所以链表比较长的话进行求和就会溢出,所以我们不能提取过之后再进行相加,
|
||||||
|
|
||||||
|
我们应该对链表的每一位进行相加,然后通过链表的和,判断是否需要像下一位进行传递,
|
||||||
|
|
||||||
|
就好比小时候我们用竖式进行加法一样,判断两位相加是否大于10,大于10则进1。
|
||||||
|
|
||||||
|
了解了思路,但是想完全实现代码也不是特别容易,这里需要注意的三个点就是,
|
||||||
|
|
||||||
|
1.我们需要根据两个链表的长度,不断对新链表添加节点
|
||||||
|
|
||||||
|
2.需要创建一个变量用来保存进位值。
|
||||||
|
|
||||||
|
3.当跳出循环之后,需要根据进位值来判断需不需要再对链表长度加1.
|
||||||
|
|
||||||
|
这三条可以结合代码理解进行。
|
||||||
|
|
||||||
|
注:进位值只能是0或1,因为每一位最大为9,9+9=18;
|
||||||
|
|
||||||
|
![链表求和](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/photo/链表求和.1yh4ymdee3k0.gif)
|
||||||
|
|
||||||
|
注:这里需要注意得时,链表遍历结束,我们应该跳出循环,但是我们的nlist仍在尾部添加了1节点,那是因为跳出循环时,summod值为1,所以我们需要在尾部再添加一个节点。
|
||||||
|
|
||||||
|
```java
|
||||||
|
class Solution {
|
||||||
|
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
|
||||||
|
//返回链表
|
||||||
|
ListNode nList = new ListNode(-1);
|
||||||
|
ListNode tempnode = nList;
|
||||||
|
//用来保存进位值,初始化为0
|
||||||
|
int summod = 0;
|
||||||
|
while(l1 != null || l2 != null) {
|
||||||
|
//如果l1的链表为空则l1num为0,若是不为空,则为链表的节点值
|
||||||
|
//判断是否为空,为空就设为0
|
||||||
|
int l1num = l1 == null ? 0 : l1.val;
|
||||||
|
int l2num = l2 == null ? 0 : l2.val;
|
||||||
|
//将链表的值和进位值相加,得到为返回链表的值
|
||||||
|
int sum = l1num+l2num+summod;
|
||||||
|
//更新进位值,例18/10=1,9/10=0
|
||||||
|
summod = sum/10;
|
||||||
|
//新节点保存的值,18%8=8,则添加8
|
||||||
|
sum = sum%10;
|
||||||
|
//添加节点
|
||||||
|
tempnode.next = new ListNode(sum);
|
||||||
|
//移动指针
|
||||||
|
tempnode = tempnode.next;
|
||||||
|
if (l1 != null) {
|
||||||
|
l1 = l1.next;
|
||||||
|
}
|
||||||
|
if (l2 != null) {
|
||||||
|
l2 = l2.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//最后根据进位值判断需不需要继续添加节点
|
||||||
|
if (summod == 1) {
|
||||||
|
tempnode.next = new ListNode(summod);
|
||||||
|
}
|
||||||
|
return nList.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
Loading…
Reference in New Issue
Block a user