数据结构经典算法题解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构经典算法题解相关的知识,希望对你有一定的参考价值。
参考技术A 1.爬楼梯问题。假设你正在爬楼,需要n阶才能到达楼顶,每次你可以爬1或者2个台阶,你有多少种不同的方法可以爬到楼顶?注意:n为正整数。/*
思路:递归思想
1.爬楼梯只能爬1个台阶或者2个台阶
2.假设有3个台阶,f(3) = f(1) + f(2) f(2) = f(1) + f(1) ,所以满足:f(n) = f(n-1) + f(n-2)
3.所以在问题设定上是满足递归思想的。
4.递归的出口是f(2)或者f(1)
*/
int ClimbStairs(int n)
/*
动态规划法:
*/
int ClimbStairs_1(int n)
2.有主串S="abcacabdc",模式串T="abc",请查找出模式串第一次出现的位置,提示:主串和模式串均为小写字母且是合法输入。
/// 子字符串在主字符串中第一个出现的位置 保证主字符串长度大于等于子字符串
/// @param main 主字符串
/// @param sub 子字符串
int subStringAtMainString_1(char *main ,char *sub)
栈思想实现:
一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题
1 链表
链表是最基本的数据结构,面试官常常用链表来考察面试者的基本能力,而且链表相关的操作相对而言比较简单,也适合考察写代码的能力。链表的操作也离不开指针,指针又很容易导致出错。综合多方面的原因,链表题目在面试中占据着很重要的地位。
以下内容思路主要参考:算法面试题 | 链表问题总结
1.1 常见题型及解题策略
1.1.1 LeetCode中关于链表的题目有以下五种类型题:
- 删除链表节点
- 反转链表
- 合并链表
- 排序链表
- 环形链表
1.1.2 解题策略
- dummy虚拟头节点,专门处理头结点可能会被改动的情况
- 快慢双指针
1.2 链表的基本内容
推荐一篇文章:基础知识讲的很清楚,Java数据结构与算法之链表
链表的分类:单链表(分带头结点和不带头结点的单链表,就是head里面有没有data的区别)、双向链表、循环链表
重点理解指针的概念
如下代码 ans.next 指向什么?
ans = ListNode(1)
ans.next = head //ans.next 指向取决于最后切断 ans.next 指向的地方在哪,所以ans.next指向head
head = head.next //ans 和 head 被切断联系了
head = head.next
ans = ListNode(1)
head = ans// ans和head共进退
head.next = ListNode(3)
head.next = ListNode(4)
// ans.next 指向什么?ListNode(3)
ans = ListNode(1)
head = ans //head 和 ans共进退
head.next = ListNode(3)
head = ListNode(2) //head 和 ans 的关系就被切断了
head.next = ListNode(4)
居然找到人跟我一样卡在这里了,笑死
1.2.1 链表的基本结构:
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
1.2.2 插入新元素
//0.找到要插入的位置
temp = 待插入位置的前驱节点.next //1.先用一个临时节点把 待插位置后面的内容先存起来
待插入位置的前驱节点.next = 待插入指针 //2.将新元素插入
待插入指针.next = temp //3.再把后面的元素接到新元素的next
1.2.3 删除某个元素
待删除位置的前驱节点.next = 待删除位置的前驱节点.next.next
1.2.4 遍历单链表
当前指针 = 头指针
while 当前节点不为空 {
print(当前节点)
当前指针 = 当前指针.next
}
for (ListNode cur = head; cur != null; cur = cur.next) {
print(cur.val)
}
1.3 删除链表结点类题目
1.3.1 题解方法
- 画草图:理解指针的变动与思考逻辑!!(重要!实用!)
- 边界条件:怎么处理不会有空指针异常?在循环里放什么停止条件
- 如果是遍历链表元素,
while(node!=null)
- 如果是删除某个元素,需要,
while(node.next!=null)
- 需要考虑的仅仅是被改变 next 指针的部分,并且循环之后哪个指针在最后的节点处,就判断谁
//比如快慢指针,输出中间节点,slow和fast的指针都在变,但是fast先指向链表尾巴,所以判断 fast
//同时每个判断next.next的都必须先判断,next,才能保证 奇偶链长 中不会出现空指针异常
while(fast.next!=null && fast.next.next!=null){
slow = slow.next;
fast = fast.next.next;
}
- 只要会删除头结点,都要进行
dummy
虚指针 - 特殊的需求可以考虑结合各种工具类,比如删除重复里面,利用
HashSet
,删除倒数第k个,利用栈LinkedList
1.3.2 可能出现的问题
①
NullPointerException
,就是当前节点为空,我们还去操作它的next
;② 输出不了结果,一定是指针移动出了问题
1.3.3 题库列表:
237. 删除链表中的节点 ====面试题 02.03. 删除中间节点
203. 移除链表元素(虚拟头结点)
19. 删除链表的倒数第 N 个结点(双指针经典类型)
237、删除链表中的节点
//237.传入待删除结点,直接将当前节点的值改为next的值,next指向next.next,实现原地更新。
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
203、移除链表元素
① 如果删除的节点是中间的节点,则问题似乎非常简单:
- 选择要删除节点的前一个结点
prev
。- 将
prev
的next
设置为要删除结点的next
。② 当要删除的一个或多个节点位于链表的头部时,要另外处理
三种方法:
- 删除头结点时另做考虑(由于头结点没有前一个结点)
- 添加一个虚拟头结点,删除头结点就不用另做考虑
- 递归
- 双指针法
即便是参考别人的代码,一看就看的懂,但其实我们有时候不知道内涵,只要自己闭着眼睛敲一遍,发现了问题,才知道是怎么考虑出来的
// 执行耗时:1 ms,击败了99.79% 的Java用户
// 内存消耗:39.4 MB,击败了49.10% 的Java用户
// 时间复杂度是O(n)。空间复杂度O(1)
public ListNode removeElements(ListNode head, int val) {
//删除值相同的头结点后,可能新的头结点也值相等,用循环解决
//比如输入 [7 7 7 7] 删除7,我一开始是直接用 if,发现有些案例无法通过才知道用while的原因
while(head!=null && head.val==val){
head = head.next;
}
//因为前面是对head的操作,所以极可能最后完了,head为空,所以把判断的过程放在后面
//我本来是吧if放在删除头结点的前面打,结果报错空指针异常,所以才知道为什么判空的要放在后面
if(head==null){
return head;
}
ListNode temp = head;//临时指针
while(temp.next!=null){
if(temp.next.val==val){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return head;
}
添加一个虚拟头结点
//执行耗时:1 ms,击败了99.79% 的Java用户
//内存消耗:39.2 MB,击败了82.52% 的Java用户
//时间复杂度是O(n)。空间复杂度O(1)
public ListNode removeElements(ListNode head, int val){
// 创建虚节点
ListNode dummyNode = new ListNode(val-1);
dummyNode.next = head;
ListNode prev = dummyNode;
while(prev.next!=null){
if(prev.next.val==val){
prev.next = prev.next.next;
}else{
prev = prev.next;
}
}
return dummyNode.next;
}
递归
//时间复杂度是O(n)。递归的方法调用栈深度是n,所以空间复杂度O(n),会超时
public ListNode removeElements(ListNode head, int val){
if(head==null){
return head;
}
// 因为递归函数返回的是已经删除节点之后的头结点
// 所以直接接上在head.next,最后就只剩下判断头结点是否与需要删除的值一致了
head.next = removeElements(head.next,val);
if(head.val==val){
return head.next;
}else{
return head;
}
}
剑指 Offer 18. 删除链表的节点
双指针
// 剑指 Offer 18. 删除链表的节点
public ListNode deleteNode(ListNode head, int val){
if(head.val==val){//因为互不相等,如果头指针相等,直接返回
return head.next;
}
//双指针
ListNode pre = head;
ListNode cur = head.next;
while(cur!=null && cur.val!=val){//找元素
pre = cur;
cur = cur.next;
}
if(cur!=null){//找到了,进行删除操作
pre.next = cur.next;
}
return head;//删完了,返回
}
面试题 02.01. 移除重复节点
// 面试题 02.01. 移除重复节点
// 法一:借助HashSet的特征
// 移除未排序链表中的重复节点。保留最开始出现的节点,重复的元素不一定连续
public ListNode removeDuplicateNodes(ListNode head) {
if (head == null) {
return head;
}
ListNode temp = head;
HashSet<Integer> set = new HashSet<>();
set.add(head.val);
while(temp.next!=null){
if(set.add(temp.next.val)){//加进去说明不重复
temp = temp.next;
}else{
temp.next = temp.next.next;//原地删除
}
}
return head;
}
// 法二:用空间换时间
// 双重循环,一个定位一个遍历后序,用时间换空间
public ListNode removeDuplicateNodes(ListNode head) {
if(head==null){
return head;
}
ListNode pre = head;
while(pre!=null){
ListNode cur = pre;
while(cur.next!=null){
if(cur.next.val==pre.val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
pre = pre.next;
}
return head;
}
82. 删除排序链表中的重复元素 II
//升序链表,删除链表中所有重复的节点【1 1 1 1 2 3】-->【2 3】
//双指针记录pre 用cur记录相同的数,加虚头节点
public ListNode deleteDuplicates(ListNode head) {
if(head==null){
return head;
}
ListNode dummy = new ListNode(0);//可能删除头结点,所以使用虚节点
dummy.next = head;
ListNode pre = dummy;
ListNode cur = dummy.next;
while(cur!=null && cur.next!=null){//画图最好理解
if(cur.val==cur.next.val ){
//如果有奇数个相同的值,就删不完,所以必须用while循环
while(cur!=null && cur.next!=null && cur.val==cur.next.val ){
cur = cur.next;//找到最后一个相等的数
}
pre.next = cur.next;
cur = pre.next;
}else{
pre = cur;
cur = cur.next;
}
}
return dummy.next;
}
19、删除链表的倒数第 N 个结点
// 删除链表的倒数第 n 个结点,并且返回链表的头结点
// 双指针
public ListNode removeNthFromEnd(ListNode head, int k){
if(head==null) return head;
// 可能会删除头结点
ListNode dummy = new ListNode(0,head);
ListNode pre = dummy.next;
for (int i = 0; i < k; i++) {
pre = pre.next;
}
ListNode cur = dummy;
while(pre!=null){
cur = cur.next;
pre = pre.next;
}
cur.next = cur.next.next;
return dummy.next;
}
// 另外一个方法,利用栈的先进后出特点,效率会更低
// 执行耗时:1 ms,击败了19.42% 的Java用户
// 内存消耗:37.7 MB,击败了5.02% 的Java用户
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null){
return head;
}
ListNode dummy = new ListNode(0,head);
ListNode temp = dummy;
LinkedList<ListNode> stack = new LinkedList<>();
while(temp!=null){
stack.push(temp);
temp = temp.next;
}
for (int i = 0; i < n; i++) {
stack.pop();
}
ListNode pre = stack.peek();
pre.next = pre.next.next;
return dummy.next;
}
876、链表的中间结点
//执行耗时:0 ms,击败了100.00% 的Java用户
//内存消耗:35.7 MB,击败了68.38% 的Java用户
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
//如果不加fast != null,链表元素个数为偶数时会报空指针异常
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
86、分隔链表
两个临时链表
// 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
// 另外创建一个链表,遍历原来的链表,删除小于的接上去。可能删除头结点
public ListNode partition(ListNode head, int x) {
ListNode small = new ListNode(0);//可能会动头结点,所以需要虚节点
ListNode smallHead = small;//要记住头结点,所以需要另外设置Head
ListNode large = new ListNode(0);
ListNode largeHead = large;
while(head!=null){
if(head.val<x){
small.next = head;
small = small.next;
}else{
large.next = head;
large = large.next;
}
head = head.next;
}
large.next = null;//再拼接两个链表,尾巴指向null
small.next = largeHead.next;
return smallHead.next;
}
328、奇偶链表(分割链表的变形)
两个临时链表的变形
// 给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。
// 请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
// 法一,利用额外空间
public ListNode oddEvenList(ListNode head) {
if(head==null){
return head;
}
ListNode odd = new ListNode(0);
ListNode oddHead = odd;
ListNode even = new ListNode(0);
ListNode evenHead = even;
int count = 1;
while(head!=null){
if(count%2==1){//奇数
odd.next = head;
odd = odd.next;
}else{
even.next = head;
even = even.next;
}
head = head.next;
count++;
}
even.next = null;
odd.next = evenHead.next;
return oddHead.next;
}
直接双指针前后遍历奇数偶数
// 不需要额外空间,双指针操作
public ListNode oddEvenList(ListNode head) {
if(head==null){
return 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;
以上是关于数据结构经典算法题解的主要内容,如果未能解决你的问题,请参考以下文章
一文通数据结构与算法之——链表+常见题型与解题策略+Leetcode经典题
数据结构与算法(Java版) | 几个经典的算法面试题(下)