剑指offer_面试题18:删除链表的节点,面试题19:正则表达式匹配,面试题20:表示数值的字符串,面试题21:调整数组顺序是奇数位于偶数前面,面试题22:链表中倒数第k个节点(链表中间节点)

面试题18:删除链表的节点
① 题目一: O ( 1 ) O(1) O(1)时间内删除链表节点

在这里插入图片描述

  • 原本的思想:
  1. 从头节点开始,遍历链表查找到待删除节点的前驱节点,更改前驱节点的next指针即可。
  2. 需要判断待删除节点的位置:是头节点、非头节点,同时还有O(n)的时间复杂度。
    在这里插入图片描述
  • 要求使用 O ( 1 ) O(1) O(1)的时间复杂度,这时有个巧妙的方法:将该节点的后继节点的值赋值给自身,更改自身的next指针以删除重复值的后继节点。
    在这里插入图片描述
  • 特殊情况: 如果待删除节点的后继节点是null,无法进行赋值,直接将待删除节点置为null即可。
  • 代码:
// 只告诉了待删除的节点,只能使用O(1)的删除算法
public void deleteNode(ListNode node) {
    // 待删除的节点是尾节点,直接将其置为null
    if (node.next == null) {
        node = null;
    } else {// 待删除的节点不是尾节点,复制后继节点的值并删除后继节点
        node.val = node.next.val;
        node.next = node.next.next;
    }
}
② 题目二:删除链表中重复的节点

在这里插入图片描述
在这里插入图片描述

  • 递归实现:
  1. 递归停止的条件为pHead == null || pHead.next == null,直接返回pHead
  2. 如果从头节点开始就存在重复节点,则不停移动头指针直到元素不重复或者后继节点为null。然后对head.next链表进行递归删除,删除后的结果作为当前链表的头节点并返回。
  3. 否则,head.next指向对head.next进行递归删除后的结果,并返回head
  • 代码如下:
public ListNode deleteDuplication(ListNode pHead) {
    if (pHead == null || pHead.next == null) {
        return pHead;
    }
    // 头节点开始就存在重复节点
    if (pHead.val == pHead.next.val) {
        while (pHead.next != null && pHead.val == pHead.next.val) {
            pHead = pHead.next;
        }
        // 指向下一个新的节点,递归删除重复节点
        return deleteDuplication(pHead.next);
    } else {// 不存在重复节点
        pHead.next = deleteDuplication(pHead.next);
        return pHead;
    }
}
③ 相似题 —— leetcode203:移除链表元素
  • 如果原链表中的节点满足要求,则直接访问下一个节点;否则,借助新的头节点,将不满足要求的节点都插入到新链表中。
  • 代码如下:
public ListNode removeElements(ListNode head, int val) {
    ListNode newHead = new ListNode(0);
    ListNode p = newHead;
    // 如果head为null不执行循环,无需进行特殊情况判断
    while (head != null) {
        if (head.val != val) {// 值不相等,将节点移动到新链表中
            p.next = head;
            p = p.next;
        }
        head = head.next;// 更新head指针,指向下一个待判断的节点
    }
    p.next = null;
    return newHead.next;
}
面试题19:正则表达式匹配

在这里插入图片描述

  • 使用动态规划:
  1. 简历二位动态数组,dp[i][j]表示 s[i]p[i] 是否匹配。
  2. 初始化:s和p都为空时,dp[0][0] = true;如果s为空且p不为空,如果p[i]*,则dp[0][i+1]=dp[0][i-1],即后退两个字符。
  3. 计算时,如果当前字符匹配或者p[j]为.,则dp[i+1][j+1]取决于前一次匹配的结果。
  4. 如果p[j]为*,则比较p中的前一个字符,不满足条件则视a*为空;其他情况,视a*为a(dp[i+1][j])、aa(dp[i][j+1])、空(dp[i+1][j-1])。
public boolean isMatch(String s, String p) {
    int m = s.length(), n = p.length();
    boolean[][] dp = new boolean[m + 1][n + 1];
    dp[0][0] = true;// s和p长度均为0的情况
    // s长度为0,p 的长度不为0
    for (int i = 0; i < n; i++) {
        if (p.charAt(i) == '*') {
            dp[0][i + 1] = dp[0][i - 1];// 后退2个字符
        }
    }
    // s和p均不为0的情况
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') {
                dp[i + 1][j + 1] = dp[i][j];// 同时匹配,结果取决于前一次结果
            }else if (p.charAt(j) == '*') {// p中的字符为*,比较前一个字符
                if (s.charAt(i) != p.charAt(j-1) && p.charAt(j-1) != '.') {
                    dp[i + 1][j + 1] = dp[i + 1][j - 1];// 跳过p中的字符
                } else {
                    dp[i + 1][j + 1] = (dp[i + 1][j]// a*作为单独的a
                            || dp[i + 1][j - 1]// a*作为空
                            || dp[i][j + 1]);// a*作为aa
                }
            }
        }
    }
    return dp[m][n];
}
  • 使用Java API,只有一句话,但是时间消耗了120ms,而动态规划只消耗了9ms。
public boolean isMatch(String s, String p) {
    return s.matches(p);
}
面试题20:表示数值的字符串

在这里插入图片描述

  • 根据示例,可以知道:
  1. 首尾的空格不影响字符串的判断,而中间的空格则影响字符串的判断。
  2. 应该先去除首尾的空格,然后判断字符串长度是否为0以及中间是否包含空格。
  3. 根据执行结果可以知道,可能存在2.334f的情况,这时应该为false,因此接着判断字符串是否包含f
  4. 然后将字符串按照e分割为两部分,第一部分可能为整数或小数,第二部分必须为整数,整数应该为long而非int
  5. 具体的数值转换使用包装类的valueOf()方法,如果出现NumberFormatException异常则说明格式不正确,返回false
  • 代码:
public boolean isNumber(String s) {
    // 先去除首尾的空格,如果新的字符串长度为0,
    // 或者中间含有空格,或者含有f,则格式错误
    if (s.length() == 0) {
        return false;
    }
    s = s.trim();
    if (s.contains(" ")) {
        return false;
    } else if (s.contains("f")) {
        return false;
    }
    // 按照e进行分割,分别判断字符串是否符合要求
    if (s.contains("e")) {
        String fisrt = s.substring(0, s.indexOf('e'));
        String second = s.substring(s.indexOf('e') + 1, s.length());
        return (isInteger(fisrt) || isDouble(fisrt)) && isInteger(second);
    } else if (s.contains(".")) {
        return isDouble(s);
    } else {
        return isInteger(s);
    }

}

public boolean isInteger(String s) {
    try {
        Long.valueOf(s);
    } catch (NumberFormatException e) {
        return false;
    }
    return true;
}

public boolean isDouble(String s) {
    try {
        Double.valueOf(s);
    } catch (NumberFormatException e) {
        return false;
    }
    return true;
}
面试题21:调整数组顺序是奇数位于偶数前面

在这里插入图片描述

① 不保证元素相对顺序
  • 最简单的想法: 使用双指针,一个从前往后查找偶数,一个从后往前查找奇数;都找到后,交换奇数和偶数。缺点: 无法保证奇数和奇数、偶数和偶数之间相对位置不变。
  • 代码:
public void reOrderArray(int[] array) {
    int i = 0, j = array.length - 1;
    while (i < j) {
        while (i < j && (array[i] & 1) != 0) {// 通过与1判断元素的奇偶性
            i++;
        }
        while (i < j && (array[j] & 1) == 0) {
            j--;
        }
        if (i < j) {
            int temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }
}
② 保证元素相对顺序
  • 最笨的方法,新开辟一个数组:
  1. 统计数组中的奇数的个数
  2. 将之前的数组复制给新数组,然后将新数组中的元素按照奇偶性分别从0oldLen开始放置。
  • 代码如下,时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)
public void reOrderArray(int[] array) {
    int oldCount = 0;
    for (int i = 0; i < array.length; i++) {
        if ((array[i] & 1) != 0) {
            oldCount++;
        }
    }
    int[] copyArr = Arrays.copyOf(array, array.length);
    int start = 0;
    for (int i = 0; i < copyArr.length; i++) {
        if ((copyArr[i] & 1) != 0) {
            array[start++] = copyArr[i];
        } else {
            array[oldCount++] = copyArr[i];
        }
    }
}
③ 相似题 —— leetcode328:链表中元素的调整
  • 将第一个节点视为奇节点,第二个节点视为偶节点,以此类推。将奇节点放在偶节点的前面,并保持原有的顺序不变。
  • 穿针法: old指针每次将奇节点穿起来,even指针每次将偶节点穿起来,最后将old指针指向偶链表的头部。停止穿针的条件:even或者even.nextnull
    在这里插入图片描述
  • 代码如下:
public ListNode oddEvenList(ListNode head) {
    if (head == null || head.next == null) {//可以只考虑head为null的情况
        return head;
    }
    ListNode old = head, even = head.next;
    ListNode evenStart = even;// 保存偶链的头节点,用于奇链和偶链的对接
    while (even != null && even.next != null) {
        // 先穿奇节点,再穿偶节点
        old.next = old.next.next;
        even.next = even.next.next;
        old = old.next;
        even = even.next;
    }
    // 两链对接
    old.next = evenStart;
    return head;
}
面试题22:链表中倒数第k个节点

在这里插入图片描述

  • 笨办法:两次遍历。
  1. 第一次遍历:统计链表中的节点个数n
  2. 第二次遍历:倒数第k个节点,即正数的n - k + 1个节点,找到n - k + 1个节点即可。
  • 快慢指针:
  1. 快指针先走k步,然后快慢指针一起移栋,直到快指针指向null节点。
  2. 如果快指针在走k步的过程中,达到了null节点,说明倒数第k个节点不存在,直接返回null
    在这里插入图片描述
public ListNode FindKthToTail(ListNode head, int k) {
    ListNode fast = head;
    while (fast != null && k > 0) {// 快指针先走k步
        fast = fast.next;
        k--;
    }
    if (k > 0) {// 倒数第k个节点超过链表长度
        return null;
    }
    ListNode slow = head;
    while (fast != null) {// 快慢指针一起走
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}
题目变形:leetcode876:求链表的中间节点
  • 快慢指针: 快指针每次走两步,慢指针每次走一步,当fastfast.nextnull时,慢指针走到了中间节点。
  • 题目的特殊之处:
  1. 链表非空,不用进行特殊情况的判断。
  2. 如果中间节点有两个,必须返回第二个。这样的话,如果fast.nextnull时,fast只能走一步。
  • 代码如下:
public ListNode middleNode(ListNode head) {
   ListNode fast = head.next, slow = head;
    // 要求同样的中间节点返回第二个
    while (fast != null) {
        if (fast.next != null) {
            fast = fast.next.next;
        } else {
            fast = fast.next;
        }
        slow = slow.next;
    }
    return slow;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值