剑指offer(16-26)
前言
【代码的规范性、完整性、鲁棒性】
- 代码的基本功能(功能测试)
- 输入边界值是否能得到正确处理(边界测试)
- 能否判断输入是否合乎规范,并对不符合规范的输入予以合适的处理(负面测试)
16. 数值的整数次方
【题目】
给定一个double
类型的浮点数base
和int
类型的整数exponent
。求base
的exponent
次方。
保证base
和exponent
不同时为 0,不得使用库函数,同时不需要考虑大数问题
【思路】
注意:base = 0 的情况要处理,exponent 为负数时要另外处理
public class Solution {
public double Power(double base, int exponent) {
if(exponent == 0){
return 1.0; // 0的0次方在数学上无意义,为 0 或 1 都可以
}
if(exponent == 1){
return base;
}
boolean isNegative = false;
if(exponent < 0){
if(base == 0.0){
throw new RuntimeException();
}
isNegative = true;
exponent = -exponent;
}
double pow = Power(base, exponent >> 1) * Power(base, exponent >> 1); // 位运算替代与2为底的运算
if((exponent & 1) != 0){ // 与 1 & 判断 奇偶
pow *= base;
}
return isNegative ? 1 / pow : pow;
}
}
17. 打印从 1 到最大的 n 位数
【题目】
输入数字n,按顺序打印出从1到最大的n位十进制数。
比如输入3,则打印出1、2、3一直到最大的3位数即999.
【思路】大数问题用字符串或者char数组存储
由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用char数组。
使用回溯法得到所有解。
result = [] def backtrack(已选择参数,待选择参数): 可以需要将参数处理成已选择内容和待选择内容; 排除不合法选择 if 满足结束条件: result.add(已选择内容) return for 选择 in 待选择内容: 排除不合法选择 做选择,记录选择 backtrack(已选择参数,待选择参数) //继续选择 撤销选择,删除记录
public class Solution {
public void print1ToMax(int n){
if (n <= 0){
return;
}
char[] number = new char[n];
}
private void backtrack(char[] number, int length){
//结束条件
if(length == number.length){
printNumber(number);
}
for(int i = 0; i < 10; i ++){
number[length] = (char) (i + '0');
backtrack(number, length++);
}
}
private void printNumber(char[] number){
int index = 0;
while(index < number.length && number[index] == '0'){
index++;
}
while(index < number.length){
System.out.print(number[index++]);
}
System.out.println();
}
}
18.1 在 O(1) 时间内删除链表节点
【题目】
给定单链表的头指针和一个节点指针,定义一个函数在O(1)的时间内删除该节点
【思路】
- 如果该节点不是尾节点,则直接将下一个节点的值赋给该节点,然后令该节点的指向下下个节点,然后删除下个节点(O(1))
- 如果该节点没有下一个节点,则需要重头找到其上一个节点
- 假设要进行 n 次操作,第一种情况为 (n - 1) * 1,第二种为 n,平均为 (2n - 1)/ n,所以为 O(1)
public class Solution {
public ListNode deleteNode(ListNode head, ListNode tobeDelete){
//链表为空或只有一个节点
if(head == null || tobeDelete == null || head.next == null){
return null;
}
//有下一个节点
if(tobeDelete.next != null){
tobeDelete.value = tobeDelete.next.value;
tobeDelete.next = tobedelete.next.next;
}else {
ListNode cur = head;
while(cur.next != tobeDelete){
cur = cur.next;
}
cur.next = tobeDelete.next
}
return head;
}
}
18.2 删除链表中重复的结点
【题目】
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。
例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
【思路】
双指针,辅助头
- 加一个辅助头节点(令其值不等于原头节点)
- 双指针
- q找变化点和终点;p与q之间判断变化点和终点前 q 的是否有重复值
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode deleteDuplication(ListNode pHead)
{
if (pHead == null || pHead.next == null){
return pHead;
}
//辅助头节点
ListNode head = new ListNode(pHead.val - 1); // 保证和第一个节点不相等;
head.next = pHead;
//双指针
ListNode pre = head;
ListNode cur = head.next;
while(cur != null){
while(cur.next != null && cur.val == cur.next.val){ // 寻找值的变化点 或 终点
cur = cur.next;
}
if(pre.next == cur){ //变化点或终点前相等的 cur 只有一个,比较的不是值,而是对象地址
pre = cur;
cur = cur.next;
}else {
pre.next = cur.next;
cur = pre.next;
}
}
return head.next; //记得去掉辅助头节点
}
}
19. 正则表达式匹配
【题目】
请实现一个函数用来匹配包括 ’ . ’ 和 ’ * ’ 的正则表达式。模式中的字符 ’ . ’ 表示任意一个字符,而 ’ * ’ 表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。
例如,字符串 “a a a” 与模式 “a . a” 和 “ab * a c * a” 匹配,但是与 “a a . a” 和 “a b * a” 均不匹配
【思路】
回溯法
匹配条件:两个都到结尾处
主要分成两种情况:
- 第二个字符是‘ * ’
- 第一个字符匹配或为‘ . ‘
- ’ * ‘ 表示 0
- ’ * ‘ 表示 1
- ’ * ‘ 表示多个
- 第一个字符不匹配
- 第二个字符不是 ‘ * ’
public class Solution {
public boolean match(char[] str, char[] pattern)
{
if(str == null || pattern == null){
return false;
}
return matchChar(str, pattern, 0, 0);
}
private boolean matchChar(char[] str, char[] pattern, int s, int p){
//满足结束条件
if(s >= str.length && p >= pattern.length){
return true;
}
if(s < str.length && p >= pattern.length){
return false;
}
//做选择 backtrack
if((p + 1) < pattern.length && pattern[p + 1] == '*'){
if(s < str.length && (str[s] == pattern[p] || pattern[p] == '.')){
return (matchChar(str, pattern, s + 1, p + 2) || matchChar(str, pattern, s, p + 2) || matchChar(str, pattern, s + 1, p));
}else{
return matchChar(str, pattern, s, p + 2);
}
}else if(s < str.length && (str[s] == pattern[p] || pattern[p] == '.')){
return matchChar(str, pattern, s + 1, p + 1);
}else {
return false;
}
}
}
20. 表示数值的字符串
【题目】
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
例如,字符串 “+100”, “5e2”, “-123”, “3.1416” 和 “-1E-16” 都表示数值。 但是 “12e”, “1a3.14”, “1.2.3”, “±5” 和 “12e+4.3” 都不是。
【思路】
正则表达式
public class Solution {
public boolean isNumeric(char[] str) {
if(str == null){
return false;
}
return new String(str).matches("[+-]?[0-9]*(\\.[0-9]*)?([eE][+-]?[0-9]+)?");
}
}
21. 调整数组顺序使奇数位于偶数前面
【题目】
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
【思路】
不稳定:首尾双指针
稳定
//不稳定 O(N)+O(1)
public class Solution {
public void reOrderArray(int [] array) {
if(array == null || array.length == 0 || array.length == 1){
return;
}
int begin = 0;
int end = array.length - 1;
while(begin < end){
if(!isOdd(array[begin]) && isOdd(array[end])){
swap(array, begin++, end--);
continue;
}else if (isOdd(array[begin])){
begin++;
}else if (!isOdd(array[end])){
end--;
}
}
}
private boolean isOdd(int value){
return (value & 1) == 1? true : false;
}
private void swap(int[] arr, int i, int j){
arr[i] = arr[i] + arr[j];
arr[j] = arr[i] - arr[j];
arr[i] = arr[i] - arr[j];
}
}
//稳定 O(N)+O(N)
public class Solution {
public void reOrderArray(int [] array) {
if(array == null || array.length < 2){
return;
}
int[] res = new int[array.length];
int begin = 0, end = array.length - 1;
for(int i = 0, j = array.length - 1; i < array.length; i++, j--){
if(isOdd(array[i])){
res[begin++] = array[i];
}
if(!isOdd(array[j])){
res[end--] = array[j];
}
}
for(int i = 0; i < array.length; i++){
array[i] = res[i];
}
}
private boolean isOdd(int val){
return (val & 1) == 1;
}
}
22. 链表中倒数第 K 个结点
【题目】
输入一个链表,输出该链表中倒数第k个结点。
【思路】
双指针
//我的
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
if(head == null || k == 0){ // 【错误】:没有考虑到倒数第0个
return null;
}
ListNode p1 = head;
ListNode p2 = head;
for(int i = 1; i < k; i++){
if(p2.next != null){
p2 = p2.next;
}else {
return null;
}
}
while(p2.next != null){
p1 = p1.next;
p2 = p2.next;
}
return p1;
}
}
//简洁版
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
ListNode p = head, q = head;
int i = 0;
while(q != null){
if(i >= k){
p = p.next;
}
q = q.next;
i++;
}
return i >= k ? p : null;
}
}
23. 链表中环的入口结点
【题目】
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
【思路】
双指针:快慢指针
相遇则有环,将快指针指向头节点变慢指针,等待下一次相遇
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead)
{
if(pHead == null){
return null;
}
ListNode p1 = pHead, p2 = pHead;
while(p2 != null && p2.next != null){
p1 = p1.next;
p2 = p2.next.next;
if(p1 == p2){
p2 = pHead;
while(p1 != p2){
p1 = p1.next;
p2 = p2.next;
}
return p2;
}
}
return null;
}
}
24. 反转链表
【题目】
输入一个链表,反转链表后,输出新链表的表头。
【思路】
pre 记录前一个节点,初始化为 null,
cur 记录当前节点,初始化为head,
next 用于暂时存储cur的下一个
//单链表
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null){
return head;}
ListNode pre = null, cur = head, next = null;
while(cur != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
//双链表
public class Solution{
public ListNode ReverseBiList(ListNode head){
ListNode pre = null, cur = head, next = null;
while(cur != null){
next = cur.next;
cur.next = pre;
cur.prior = next;
pre = cur;
cur = next;
}
}
}
25. 合并两个排序的链表
【题目】
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
【思路】
面对头节点不确定问题:辅助头节点
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
ListNode preHead = new ListNode(-1);
ListNode pre = preHead;
while(list1 != null && list2 != null){
if(list1.val <= list2.val){
pre.next = list1;
pre = pre.next;
list1 = list1.next;
}else{
pre.next = list2;
pre = pre.next;
list2 = list2.next;
}
}
while(list1 != null){
pre.next = list1;
break; // 【错误处】:没有结束循环
}
while(list2 != null){
pre.next = list2;
break;
}
return preHead.next;
}
}
25. 树的子结构
【题目】
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
【思路】
二叉树的问题一般都用递归求解
重点是确定最终成功与否的最终状态,即结束条件(递归出口)
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root1 == null || root2 == null) return false;
return doesTree1HasTree2(root1, root2);
}
private boolean doesTree1HasTree2(TreeNode root1, TreeNode root2){
//满足结束条件
if(root2 == null){
return true;
}
if(root1 == null){
return false;
}
// 做选择
if(root2.val == root1.val){
if (doesTree1HasTree2(root1.left, root2.left) && doesTree1HasTree2(root1.right, root2.right)){
return true; //【错误点】:直接return 结果,显示出错,用if正确
}
}
return (doesTree1HasTree2(root1.left, root2) || doesTree1HasTree2(root1.right, root2));
}
}
1, root2);
}
private boolean doesTree1HasTree2(TreeNode root1, TreeNode root2){
//满足结束条件
if(root2 == null){
return true;
}
if(root1 == null){
return false;
}
// 做选择
if(root2.val == root1.val){
if (doesTree1HasTree2(root1.left, root2.left) && doesTree1HasTree2(root1.right, root2.right)){
return true; //【错误点】:直接return 结果,显示出错,用if正确
}
}
return (doesTree1HasTree2(root1.left, root2) || doesTree1HasTree2(root1.right, root2));
}
}