搜索算法
剑指 Offer 04. 二维数组中的查找(简单)
在一个 n * m 的二维数组中,每一行都按照从左到右 非递减 的顺序排序,每一列都按照从上到下 非递减 的顺序排序.
请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
现有矩阵 matrix 如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
解题:暴力遍历
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
for(int i = 0;i<matrix.length;i++){
for(int j = 0; j<matrix[0].length;j++){
if(matrix[i][j] == target) return true;
}
}
return false;
}
}
解题:搜索
/**
发现某个元素有一定规律:上边的元素都比他小 右边的元素都比他大,那么就可以从左下角进行搜索
因此,可以从左下角进行搜索,当前元素如果比target小,那就向右移动.当前元素如果比target大,那就向上移动.
**/
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix.length == 0)
return false;
int x = 0;
int y = matrix.length-1;
while(y>=0 && x<matrix[0].length){
if(matrix[y][x] > target)
y--;
else if(matrix[y][x] < target)
x++;
else
return true;
}
return false;
}
}
剑指 Offer 11. 旋转数组的最小数字(简单)
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。
请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为 1。
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
示例 1:
输入:numbers = [3,4,5,1,2]
输出:1
示例 2:
输入:numbers = [2,2,2,0,1]
输出:0
解题 顺序搜索
// 其实就是遍历数组找最小值的过程
class Solution {
public int minArray(int[] numbers) {
int min = numbers[0];
for(int i= 0; i<numbers.length-1;i++){
if(numbers[i] > numbers[i+1]){
if(numbers[i+1]<min)
min = numbers[i+1];
}
}
return min;
}
}
解题 二分查找
由于算法是局部递增的,因此可以尝试使用二分查找,不过要改变一些判断条件.可以大致分为以下三种情况:
/**
设置界限下标:left right mid
if number[mid[ > number[right] 说明肯定在右边 [mid,right]中 因此,left = mid + 1 (如图1)
if number[mid[ < number[right] 说明肯定在左边 [left,mid]中 因此,right = mid (如图2)
if number[mid] = number[right] right-- (如图3)
tip:为什么right--而不是left++
因为最小值一定是出现在偏左边的位置,如果使用left++ 可以把最小的值给错过了
比如1,2,2
**/
class Solution {
public int minArray(int[] numbers) {
int left = 0, right = numbers.length-1;
while(left<right){
int mid = (left+right)/2;
if(numbers[mid] > numbers[right]) left = mid + 1;
else if(numbers[mid] < numbers[right]) right = mid;
else right --;
}
return numbers[left];
}
}
剑指 Offer 53 - I. 在排序数组中查找数字 I(简单)
统计一个数字在排序数组中出现的次数。
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
解题 暴力搜索
/**
从左到右进行遍历即可
又由于数组元素递增,因此当遍历的元素大于target值的时候跳出循环,可以节省一些时间。
**/
class Solution {
public int search(int[] nums, int target) {
int count = 0;
for(int i = 0; i< nums.length; i++){
if(nums[i] == target)
count += 1;
if(nums[i] > target)
break;
}
return count;
}
}
解题 二分查找
/**
算法思路:因为升序序列中存在重复的值 所以使用普通的二分查找
因此可以转换思路,找某个数值的左右界限。
以右界限为例:
taget的个数刚好是taget的右界限 - target-1的右界限
左节点同理:taget+1的左界限 - taget的左界限
(仅需变动一下等于号的位置和return值即可)
**/
class Solution {
public int BinarySearch(int[] nums, int target){
int left = 0, right = nums.length-1, mid;
while(left <= right){
mid = (left+right) / 2;
if(nums[mid] <= target)
left = mid+1;
else if(nums[mid] > target)
right = mid-1;
}
return right;
}
public int search(int[] nums, int target) {
return BinarySearch(nums,target) - BinarySearch(nums,target-1);
}
}
剑指 Offer 53 - II. 0~n-1 中缺失的数字(简单)
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
输入: [0,1,3]
输出: 2
输入: [0,1,3]
输出: 2
解题 二分搜索
/**
首先这个数组长得非常有特点,又是递增的,就很容易想到二分查找,那么中间值就只有两种情况了,要么是等于下标 要么是不等于下标。
如果等于下标:那么缺失值就在右半个区间
如果不等于下标:那么缺失值就在左半个区间
这样返回界限值left即可。
**/
class Solution {
public int missingNumber(int[] nums) {
int left = 0, right = nums.length-1,mid;
while(left<=right){
mid = (left+right)/2;
if(nums[mid] == mid)
left = mid+1;
else
right = mid-1;
}
return left;
}
}
栈和队列
剑指 Offer 06. 从尾到头打印链表(简单)
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
输入:head = [1,3,2]
输出:[2,3,1]
解题 栈的特点
/**
遍历单链表,入栈每一个元素
再出战每一个元素,并存到数组里
**/
class Solution {
public int[] reversePrint(ListNode head) {
Stack<ListNode> stack = new Stack<ListNode>();
ListNode p = head;
while(p!=null){
stack.push(p);
p = p.next;
}
int length = stack.size();
int[] result = new int[length];
for(int i=0;i<length;i++){
result[i] = stack.pop().val;
}
return result;
}
}
剑指 Offer 09. 用两个栈实现队列(中等)
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
输入:
["CQueue","appendTail","deleteHead","deleteHead","deleteHead"]
[[],[3],[],[],[]]
输出:[null,null,3,-1,-1]
输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]
解题 两个栈
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<Integer>();
stack2 = new Stack<Integer>();
}
public void appendTail(int value) {
stack1.push(value);
}
public int deleteHead() {
if(stack2.isEmpty())
while(!stack1.isEmpty())
stack2.push(stack1.pop());
if(stack2.isEmpty())
return -1;
else
return stack2.pop();
}
}
链表
剑指 Offer 18. 删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解题 遍历删除
/**
由于删除第i个元素需要一个指向第i-1个元素的指针。
因此仅需要判断遍历指针p的p.next的值是否是要被删除的元素即可。
*/
class Solution {
public ListNode deleteNode(ListNode head, int val) {
ListNode p = head;
if(p.val == val){
head = head.next;
}
else{
while(p.next!=null){
if(p.next.val == val){
ListNode q = p.next;
p.next = q.next;
break;
}
p = p.next;
}
}
return head;
}
}
剑指 Offer 22. 链表中倒数第 k 个节点
输入一个链表,输出该链表中倒数第k个节点。
为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。
这个链表的倒数第 3 个节点是值为 4 的节点
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
解题 双指针遍历
/**
设置两个指针p和q。
先让p走k步,然后再让p和q一直走,他们之间相距的距离就是k个距离。
当p走到尽头的时候,q指向的刚好是倒数第k个元素。
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode p = head;
while(k!=0){
p = p.next;
k--;
}
ListNode q = head;
while(p!=null){
q = q.next;
p = p.next;
}
return q;
}
}
剑指 Offer 24. 反转链表
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
解题 头插法
/**
头插法逆置单链表
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null){
return null;
}
ListNode p = head.next;
head.next = null;
while(p!=null){
ListNode q = p.next;
p.next = head;
head = p;
p = q;
}
return head;
}
}
剑指 Offer 25. 合并两个排序的链表(解题有新感悟)
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
解题 递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode p = l1;
ListNode q = l2;
if(l1 == null)
return l2;
if(l2 == null)
return l1;
if(l1.val<=l2.val){
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
}
贴两个对递归代码理解的图
剑指 Offer 52. 两个链表的第一个公共节点
输入两个链表,找出它们的第一个公共节点。
解题 双指针
/**
定义两个指针curA和curB,让他俩循环的走这两个单链表。
当headA走到头就去走headB,haedB走到头就去走headA。
假设,headA长度是a,headB长度是b。
那么当两个指针都走了长为a+b的路径后,就会相遇。
*/
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
while (curA != curB) {
curA = curA != null ? curA.next : headB;
curB = curB != null ? curB.next : headA;
}
return curA;
}
}
其他算法
剑指 Offer 05. 替换空格(简单)
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
解题 java自带函数
class Solution {
public String replaceSpace(String s) {
return s.replace(" ","%20");
}
}
剑指 Offer 17. 打印从 1 到最大的 n 位数(简单)
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
解题 遍历
// 10的n次幂再-1就是这个数组的最大数,接下来遍历即可
class Solution {
public int[] printNumbers(int n) {
int max = (int) Math.pow(10,n);
int[] array = new int[max-1];
for(int i = 1; i < max; i++){
array[i-1] = i;
}
return array;
}
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面(简单)
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分.
输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。
解题 暴力遍历
// 先遍历原本的数组,将奇数和偶数分开来保存,再将奇数和偶数先后存到新的数组里。
class Solution {
public int[] exchange(int[] nums) {
int[] jishu = new int[nums.length];
int[] oushu = new int[nums.length];
int x = 0, y = 0;
for(int i = 0; i < nums.length ; i++){
if(nums[i] % 2 == 0){
oushu[x] = nums[i];
x++;
}else{
jishu[y] = nums[i];
y++;
}
}
int[] result = new int[x+y];
System.arraycopy(jishu, 0, result, 0, y);
System.arraycopy(oushu, 0, result, y, x);
return result;
}
}
解题 双指针变量法
/**
中心思想:奇数在前 偶数在后
那么设置两个指针变量 start end 分别指向第一个元素和最后一个元素
如果start的元素不是奇数,那他就和end交换
如果end的元素不是偶数,那他就和奇数交换
如果start是奇数就往后走,end是偶数就往前走
当整个数组被遍历一遍以后,就可以得到最终的奇数在前偶数在后的数组了。
**/
class Solution {
public int[] exchange(int[] nums) {
int start = 0, end = nums.length-1;
while(start <= end){
if(nums[start]%2 != 0)
start ++;
else if(nums[end]%2 == 0)
end --;
else{
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
}
}
return nums;
}
}
剑指 Offer 29. 顺时针打印矩阵(中等偏难)
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
解题 定义bottom、top、left、right进行循环遍历
/**
定义四个状态,每打印一行为一个状态,四个状态为一圈
设置四个边界遍历,其实也指四个状态对应的遍历的行和列
top = 0 这是第一行,对应的state0
right = matrix[00.length - 1 这是最右边的列,对应state1
bottom = matrix.length - 1 这是最后一行,对应的state2
left = 0 这是最左边的列,对应state3
当上边界小于等于下边界 and 左边界小于等于右边界的时候循环输出
**/
class Solution {
public int[] spiralOrder(int[][] matrix) {
if(matrix.length == 0) return new int[0];
int bottom = matrix.length - 1 , top = 0;
int left = 0, right = matrix[0].length - 1;
int state = 0;
int i, j;
int[] result = new int[matrix.length*matrix[0].length];
int k = 0;
while(left <= right && top <= bottom){
if(state == 0){
for( j = left ; j <= right; j++){
result[k] = matrix[top][j];
k = k +1;
}
top++;
state = 1;
}else if(state == 1){
for( i = top; i<= bottom; i++){
result[k] = matrix[i][right];
k = k + 1;
}
right--;
state = 2;
}else if(state == 2){
for(j = right; j >=left; j--){
result[k] = matrix[bottom][j];
k = k + 1;
}
bottom--;
state = 3;
}else if(state == 3){
for(i = bottom; i >= top; i--){
result[k] = matrix[i][left];
k = k + 1;
}
left ++;
state = 0;
}
}
return result;
}
}
剑指 Offer 39. 数组中出现次数超过一半的数字(中等)
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
解题 摩尔投票
/**
遍历 nums 数组,使用 count 进行计数,记录当前出现的数字为 cur,如果遍历到的 num 与 cur 相等,则 count 自增,否则自减,
当其减为 0 时则将 cur 修改为当前遍历的 num,通过增减抵消的方式,最终达到剩下的数字是结果的效果,时间复杂度为 O(n).
出现次数超过一半的数字一定不会被抵消掉,最终得到了留存.
**/
class Solution {
public int majorityElement(int[] nums) {
int c = nums[0];
int count = 0;
for(int i = 1; i < nums.length; i++){
if(nums[i] == c)
count ++;
else if(count > 0)
count -- ;
else{
count = 0;
c = nums[i];
}
}
return c;
}
}
剑指 Offer 45. 把数组排成最小的数(简单偏难)
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1:
输入: [10,2]
输出: "102"
示例 2:
输入: [3,30,34,5,9]
输出: "3033459"
解题 修改排序规则
/**
首先 我们发现102 比210要小,所以,考虑排序算法
先给整数转换成字符类型然后做相加减。
如果a+b > b+a 那么我们认为 a > b
这样讲strs数组排序完毕
再做数组内遍历,将所有数字字符加在一起,就是最终的结果。
**/
class Solution {
public String minNumber(int[] nums) {
String[] strs = new String[nums.length];
for(int i = 0; i < nums.length; i++)
strs[i] = String.valueOf(nums[i]);
Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x));
String re = "";
for(String s : strs)
re = re + s;
return re;
}
}
剑指 Offer 57. 和为 s 的两个数字(简单)
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
解题 双指针遍历法
/**
首先,我尝试了两层循环嵌套遍历,毫不意外的超时了 因为时间复杂度达到了N^2
然后,回忆以前做的题,想到,可以用两个指针。
如果相加大了,那说明得找个小点的数,那就让end--
如果相加小了,那说明得找个大点的数,那就让start++
时间复杂度只要O(N)就可以了
**/
class Solution {
public int[] twoSum(int[] nums, int target) {
int start = 0, end = nums.length-1;
if(nums.length < 2)
return null;
while(nums[start] + nums[end] != target && start<end){
if(nums[start] + nums[end] < target)
start ++;
else
end --;
}
int[] result = {nums[start],nums[end]};
return result;
}
}
剑指 Offer 57 - II. 和为 s 的连续正数序列(中等)
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
输入:target = 9
输出:[[2,3,4],[4,5]]
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
解题 滑动窗口
/**
滑动窗口思想
设置左右两个界限left=1和right-2
根据等差数列求和公式有:当前窗口内容为:sum = (left+right) * (right-left+1) / 2
如果sum < target值 移动右边边界right
如果sum > target值 移动左边边界left
**/
class Solution {
public int[][] findContinuousSequence(int target) {
int left = 1, right = 2;
List<int[]> res = new ArrayList<>();
while(left < right){
int sum = (left+right) * (right-left+1) / 2 ;//等差数列求和公式
if(sum == target){
int[] arr = new int[right - left + 1];
for (int k = left; k <= right; k++) {
arr[k - left] = k;
}
res.add(arr);
left++;
}else if(sum < target){
right++;
}else
left++;
}
return res.toArray(new int[res.size()][]);
}
}
剑指 Offer 58 - I. 翻转单词顺序(简单)
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。
输入: "the sky is blue"
输出: "blue is sky the"
输入: " hello world! "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
输入: " hello world! "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
解题 双指针法
/**
**/
class Solution {
public String reverseWords(String s) {
s = s.trim();
int start,end;
start = end = s.length()-1;
String result = "";
while(start >= 0){
while(start >=0 && s.charAt(start) != ' '){
start --;
}
result += s.substring(start + 1, end + 1) + " ";
while(start >= 0 && s.charAt(start) == ' '){
start--;
}
end = start;
}
return result.trim();
}
}
剑指 Offer 58 - II. 左旋转字符串(简单)
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
输入: s = "abcdefg", k = 2
输出: "cdefgab"
输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"
解题 三次旋转法
/**
408数据结构原题
首先将所有的元素进行翻转
然后再把元素从指定位置分成两个子串,分别翻转再拼接即可。
**/
class Solution {
public String reverseLeftWords(String s, int n) {
String reverse = new StringBuffer(s).reverse().toString();
String sub1 = new StringBuffer(reverse.substring(0,s.length()-n)).reverse().toString();
String sub2 = new StringBuffer(reverse.substring(s.length()-n,s.length())).reverse().toString();
return sub1 + sub2;
}
}
剑指 Offer 61. 扑克牌中的顺子(中等)
从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
输入: [1,2,3,4,5]
输出: True
输入: [0,0,1,2,5]
输出: True
解题 排序判断
// 先对数组进行排序
// 然后记录0的数组
// 判断非0元素的前后关系,若前后一致的话,则非顺子
// 若后边元素大前边的元素很多 就用0来代替
class Solution {
public boolean isStraight(int[] nums) {
// 排序
Arrays.sort(nums);
// 统计大小王的个数
int mao = 0;
for(int i = 0;i<nums.length;i++){
if(nums[i] == 0){
mao++;
continue;
}
if (i != nums.length-1) {
if (nums[i] == nums[i+1] )
return false;
}
}
return nums[nums.length-1]-nums[mao] < 5;
}
}
剑指 Offer 66. 构建乘积数组(中等偏难)
给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
输入: [1,2,3,4,5]
输出: [120,60,40,30,24]
解题 借助中间变量存储后面的乘积
/**
可以把大小为n的数组想象成一个n*n的矩阵
先算矩阵的下半部分
再算矩阵的上半部分
**/
class Solution {
public int[] constructArr(int[] a) {
int[] b = new int[a.length];
int i,left=1;
for(i=0;i<a.length;i++){
b[i] = left;
left *= a[i];
}
int right = 1;
for (i = a.length - 1; i >= 0; i--) {
b[i] *= right;
right *= a[i];
}
return b;
}
}