算法训练—leetcode—数组篇(二)

1.移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
此题相对较简单,其实我们只需要循环遍历数组,把满足条件的值(不等于目标值)依次放入数组即可
在这里插入图片描述

public int removeElement(int[] nums, int val) {
	int index = 0;
    for(int num : nums){
    	if(num != val){
        	nums[index++] = num;
        }
    }
    return index;
}

上述这种方式,其实本质是双指针的思想,我们定义了后指针为我们需要从新赋予的值的下标,前指针为遍历元素时的下标。
使用前后指针,我们可以把12345(3)变成1245,满足题意

public int removeElement(int[] nums, int val) {
	int back= 0;
    for (int before= 0; before< nums.length; before++) {
		if (nums[before] != val) {
            nums[back++] = nums[before];
        }
    }
    return back;
}

基于上述这种方式,我们还可以改为左右指针,
使用左右指针,我们可以把12345(3)变成12545,同样满足题意

public int removeElement(int[] nums, int val) {
	int left = 0;
    int right = nums.length-1;
    while (left <= right) {
        if (nums[left] == val) {
            nums[left] = nums[right];
            right--;
        } else {
            left++;
        }
    }
    return left;
}

2.移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。尽量减少操作次数。

2.1.复制非零值

在这里插入图片描述
解题要素:
1、定义一个坐标,记录当前当前移动的元素的下标
2、当循环到某个元素不为0时,将该值赋值给需要移动的元素
伪代码示意

for(nums){
	if( nums [i] 不等于0 ){
		nums [move] = nums [i];
		move ++;
	}
}
move之后的元素设置为0
public void moveZeroes(int[] nums) {
    if (nums==null) return;
	int moveIndex = 0;
	for (int i = 0; i < nums.length; i++) {
		if(nums[i] != 0) {
			nums[moveIndex] = nums[i];
            moveIndex ++;
		}
	}
    for (int i = moveIndex; i < nums.length; i++) {
		nums[i] = 0;
	}
}

在这里插入图片描述

2.2.与零交换值

解题要素:定义一个坐标,记录当前执行交换操作的坐标。定义一个第三只手temp,用于两数交换。
在这里插入图片描述
伪代码示意

for(nums){
	if( nums [i] 不等于0 ){
		temp = 交换值;
		交换值 = 循环值;
		循环值 = temp;
		move ++;
	}
}

完整代码

public void moveZeroes(int[] nums) {
	if(nums==null) return;
	int exchange = 0;
	for (int i = 0; i < nums.length; i++) {
		if(nums[i]!=0) {
			int temp = nums[exchange];
			nums[exchange ++] = nums[i];
			nums[i] = temp;
		}
	}
}

在这里插入图片描述

3.删除排序数组中的重复项

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
解题思路:这里的解题思路和移动0类似,移动0中,我们如果不等于0就复制值到前面去,这里如果不等于前面的数,我们就复制值到前面去
在这里插入图片描述
伪代码示意

for(nums){
	if( nums [move] 不等于 nums [i]){
		move ++;
		nums [move] = nums [i];
	}
}
	public int removeDuplicates(int[] nums) {
		int moveIndex = 0;
		for (int i = 0; i < nums.length; i++) {
			if(nums[moveIndex] != nums[i]) {
				moveIndex ++ ;
				nums[moveIndex] = nums[i];
			}
		}
		return moveIndex + 1;
	}

在这里插入图片描述

4.盛最多水的容器

给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。
在这里插入图片描述
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49

4.1.暴力双循环

通过如上图所示,我们不难得出,此题的关键我们可以等价于求上述长方形的最大面积。并且知道的长度(Y轴)由最短长度的决定,宽度(X轴)由两条左边相减决定。因此得出公式:面积 = Min(Y1,Y2)* (X2 –X1 );
通过双循环,穷举所有的面积,然后取最大面积值。
伪代码示意

for(1 ){
	for(2 ){
		求最小高;
		求宽度;
		S = 最小高 * 宽度
		求最大S
	}
}
 public int maxArea(int[] height) {
 	int maxArea= 0;
	for (int i = 0; i < height.length; i++) {
		for (int j = i + 1; j < height.length; j++) {
			int minheight = Math.min(height[i], height[j]);
			int area = minheight* (j-i);
			maxArea= Math.max(maxArea, area);
		}
	}
	return max;
}

在这里插入图片描述

4.2.双指针:左右指针最短收缩

在双重for循环中,我们穷举了所有的面积,但是,有没有一些方法,可以过滤掉我们提前就能判断出该面积一定是小于之前的面积?
定义左右两个指针,这时候所能形成的最长的宽,就能确定了,此时的面积,作为初始最大面积,当我们宽度在缩小的同时,如果长度也在缩小,则面积也一定缩小;有且仅有在宽度缩短的时候,长度变长,才可能比初始面积大。
解题要素
1、左右两边长度哪个小,就收缩哪个;
2、 area = length * (right - left + 1);
伪代码示意

while( left < right ){
	minHitht : left right 谁小谁收缩
	width
	s = minHitht * width
	max(s,maxs)
}
   public int maxArea(int[] height) {
       	int max = 0;
		int left = 0;
		int right = height.length-1;
		while (left < right) {
			int length = height[left]>height[right]?height[right--]:height[left++];
			int area = length * (right - left + 1);
			max = Math.max(max, area);
		}
        return max; 
    }

在这里插入图片描述

5.旋转数组

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
说明:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
要求使用空间复杂度为 O(1) 的 原地算法。

5.1.暴力:k 次整体右移一个单位

我们先实现一个操作,就是将整个数组集体向右移动一个单位,
在这里插入图片描述
因此,可以得出如下代码实现 12345转变成51234

public void rotate(int[] nums, int k) {
	int length = nums.length-1;
	int temp = nums[length];
	for (int i = length; i > 0; i--) {
		nums[i] = nums[i-1];
	}
	nums[0]=temp;
}

那么接下来我们将上面的过程循环K次,

public void rotate(int[] nums, int k) {
	for (int j = 0; j < k; j++) {
		int length = nums.length-1;
		int temp = nums[length];
		for (int i = length; i > 0; i--) {
			nums[i] = nums[i-1];
		}
		nums[0]=temp;
	}
}

我们发现其实还可以继续优化,当K大于 nums.length时候,我们出现了重复的移动
如int [] nums = {1,2,3,4,5,6};此时K = 2 + n * nums.length(n>1),其实我们只需要做K次操作就可以了,因此我们真正的循环次数是:K % nums.length次。

完整代码示意

public void rotate(int[] nums, int k) {
	for (int j = 0; j < k % nums.length; j++) {
		int temp = nums[nums.length-1];
		for (int i = nums.length -1; i >0; i--) {
			nums[i] = nums[i-1];
		}
		nums[0] = temp;
	}
}

在这里插入图片描述
**备注:**现在(20210924)暴力破解不行了,超出时间限制。
在这里插入图片描述

5.2.空间换时间:数据拷贝再还原

我们可以用一个额外的数组来将每个元素放到正确的位置上,也就是原本数组里下标为 i的我们把它放到 (i+k)%数组长度的位置。然后再把新的数组拷贝到原数组中。
伪代码示意

temp[];
将nums[i]元素放到temp[i+k)% lenth];
nums[] = temp[];

因此我们可以得到如下代码

public void rotate(int[] nums, int k) {
	int length = nums.length;
	int[] temp = new int[length];
	for (int i = 0; i < length; i++) {
		temp[(i + k) % length] = nums[i];
	}
	for (int i = 0; i < length; i++) {
		nums[i] = temp[i];
	}
}

在这里插入图片描述

5.3.反转-分段反转

首选将整个数组进行反转,然后,接着分成两段,前段部门和后段部再反转。
原始数组                  : 1 2 3 4 5 6 7反转所有数字后             : 7 6 5 4 3 2 1反转前 k 个数字后          : 5 6 7 4 3 2 1反转后 n-k 个数字后        : 5 6 7 1 2 3 4 --> 结果
解决要素:
参考反转字符串,如何进行反转

public void rotate(int[] nums, int k) {
    k = k % nums.length;
	reverse(nums, 0, nums.length-1);
	reverse(nums, 0, k-1);
	reverse(nums, k, nums.length-1);
}
public static void reverse(int[] nums, int start, int end) {
	while (start < end) {
		int temp = nums[end];
		nums[end] = nums[start];
		nums[start] = temp;
		start ++;
		end --;
	}
}

在这里插入图片描述

5.4.环状替换:依次占位

如果我们直接把每一个数字(位置为:current)移动到位置(current + k) % nums.length),我们可以把被替换的数字保存在变量 temp里面,作为下一次移动的数据。运气好的情况下,一次轮询就能移动完成,运气不好的时候,就需要换个坐标重新轮询。
当nums.length和k的最大公约数等于1的时候,1次遍历就可以完成交换
当nums.length和k的最大公约数不等于1的时候:需要最大公约数次遍历,也就是换个坐位重新移动。
如下图所示。
在这里插入图片描述

public void rotate(int[] nums, int k) {
		k = k % nums.length;
		//移动次数等于数组长度,代表移动完成
		int count = 0 ;
		for (int start = 0; count < nums.length; start++) {
			//当前移动的位置
			int current = start;
			//当前要移动的元素
			int prev = nums[start];
			do {
				//需要霸占的元素的位置
				int next = (current + k) % nums.length;
				//将需要霸占位置的元素存储到temp中
				int temp = nums[next];
				//将需要移动的元素放到需要霸占的位置上
				nums[next] = prev;
				//将temp中的元素作为下一次需要移动的元素
				prev = temp;
				//下一次需要移动元素的位置
				current = next;
				count ++;
			}while(start != current);
		}
	}

在这里插入图片描述

6.合并两个有序数组

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明:
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例:
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
提示:
-10^9 <= nums1[i], nums2[i] <= 10^9
nums1.length == m + n
nums2.length == n

6.1.遍历+合并+排序

循环nums2,将nums2的元素追加到nums1的尾部中,然后对nums1进行排序。
伪代码示意

for(第二个数组的数量){
	第一个数组的最后一个位置 = 第二个数组的第一个元素
	第一个元素的最后一个位置 ++}

完整代码

public void merge(int[] nums1, int m, int[] nums2, int n) {
	for (int i = 0; i < n; i++){
		nums1[m++] = nums2[i];
	}
	Arrays.sort(nums1);
}

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

6.2.双指针:从前往后对比

我们创建一个和nums1一样大小的数组newNums,然后,对比nums1中和nums2中的元素,将小的元素放到newNums中,当我们结束对比,说明至少有一方已经对比完毕了,剩下的一方的元素都是比新数组中的元素要大,因此将剩下的元素拷贝到新数组即可。

public void merge2(int[] nums1, int m, int[] nums2, int n) {
	int[] sorted = new int[m + n];
	int pm= 0, pn = 0, ps = 0;
	while (pm < m || pn < n) {
		if(pm == m) {
			//nums1中的元素已经比较完毕,剩下的都是nums2
			sorted[ps ++] = nums2[pn ++];
		} else if(pn == n) {
			//nums2中的元素已经比较完毕,剩下的都是nums1
			sorted[ps ++] = nums1[pm ++];
		} else if(nums1[pm] < nums2[pn]) {
			//nums1小,取nums1的元素
			sorted[ps ++] = nums1[pm ++];
		} else {
			//nums2小,取nums2的元素
			sorted[ps ++] = nums2[pn ++];
		}
	}
	for (int i = 0; i != m + n; ++i) {
        nums1[i] = sorted[i];
    }
}

7.加一

给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
示例 1:
输入: [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123。
示例 2:
输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。
解题思路:
1、尾数加1如果不等于10.则直接返回;
2、如果尾数加1等于10,则尾数变为0,并进入下一轮循环;
3、如果循环完毕,即{9,9,9}这种情况,则直接返回{1,0,0,0}

public int[] plusOne(int[] digits) {
	for (int i = digits.length-1; i >= 0; i--) {
		digits[i] = digits[i] +1;
		if(digits[i] != 10) {
			return digits;
		}
		digits[i] = 0;
	}
	int[] newDigits = new int[digits.length + 1];
	newDigits[0] = 1;
	return newDigits;
}

在这里插入图片描述

8.有效字母异位词

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1:
输入: s = “anagram”, t = “nagaram”
输出: true
示例 2:
输入: s = “rat”, t = “car”
输出: false
说明:
你可以假设字符串只包含小写字母。
进阶:
如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
借用外部空间排序对比
解题要素:
1、什么是异位词:两个字符串的组成元素一样,只是组成元素的位置不一样。
2、既然组成元素一样,只是位置不一样,则可以对两个字符串进行排序,这样组成元素和位置都一样了

public boolean isAnagram(String s, String t) {
	if(s.length() != t.length()) return false;
	char[] charArrayS = s.toCharArray();
	char[] charArrayT = t.toCharArray();
	Arrays.sort(charArrayS);
	Arrays.sort(charArrayT);
	//return Arrays.equals(charArrayS, charArrayT);
	for (int i = 0; i < charArrayT.length; i++) {
		if(charArrayS[i] != charArrayT[i]) {
			return false;
		}
	}
	return true;
}

在这里插入图片描述
hash比对
依据题意,只会出现小写字母,因此我们可以映射如下的hash,当然这里并不是直接使用hash表,而是借助hash的思想,使用当前小写字母减去a的ASCII的值,做Key,出现的次数做value。则我们可以形成如下索引映射表。
索引0映射a,索引1映射b…索引25映射z
。这样,我们设计一个计数器,当我们遍历s字符串的时候,记录对应索引中元素出现的次数,当我们遍历t字符串的时候,减去记录。如果两个字符串的组成元素相同的话,那么如下这个hash思想所建立的数组的,所有元素的值,都为0。(如果两个字符串的元素都相同,那么一边加,一边减,最后肯定减为0)
在这里插入图片描述

	public boolean isAnagram(String s, String t) {
		if(s.length() != t.length()) return false;
		int[] count = new int[26];
		for (int i = 0; i < s.length(); i++) {
			count[s.charAt(i)-'a'] ++;
			count[t.charAt(i)-'a'] --;
		}
		for (int i = 0; i < count.length; i++) {
			if(count[i] != 0) return false;
		}
		return true;
	}

在这里插入图片描述

9.螺旋矩阵

给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例 1:
输入:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
输出: [1,2,3,6,9,8,7,4,5]
示例 2:
输入:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]
解题要素:
设定上下左右四个边界值:
左:从0开始,循环一轮后,左++,结束标志为是否需要从上到下循环;
右:从右开始(二维数组中,一维数组的最大长度),循环一轮后,右- -,结束标志为是否需要从右到左循环;
上:从0开始,循环一轮后,上++,结束标志为是否需要从下到上循环;
下:从下开始,循环一轮后(二维数组中,二维数组的总个数),下- -,结束标志为是否需要从左到右循环;
以左到有循环为例:保持上(top)不变,依次读取:matrix[top][0],matrix[top][1],matrix[top][2],当循环一轮完毕后,判断上(top)+1 的边界值和下边界值(bottom )是否相等,如果相等,说明所有元素已经读取完毕,则结束循环。重复读取。知道边界值相等。

public List<Integer> spiralOrder(int[][] matrix) {
    List<Integer> result = new ArrayList<>();
    if(matrix.length == 0) {
        return result;
    }
    int left = 0, right= matrix[0].length - 1, top = 0, bottom = matrix.length - 1, resultIndex = 0;
    //int[] result = new int[(right + 1) * (bottom + 1)];
    while (true){
        //从左向右移动
        for(int i = left; i <= right; i++) {
            //result[resultIndex++] = matrix[top][i];
            result.add(matrix[top][i]);
        }
        //右移边界值:向右移动完毕后,判断是否执行从上向下移动
        if(top++ == bottom) break;

        //从上向下移动
        for(int i = top; i <= bottom; i++) {
            //result[resultIndex++] = matrix[i][right];
            result.add(matrix[i][right]);
        }
        //下移边界值:向下移动完毕后,判断是否执行从右向左移动
        if(left == right--) break;

        //从右向左移动
        for(int i = right; i >= left; i --) {
            //result[resultIndex++] = matrix[bottom][i];
            result.add(matrix[bottom][i]);
        }
        //左移边界值:向左移动完毕后,判断是否执行从下向上移动
        if(top == bottom-- ) break;

        //从下向上移动
        for(int i = bottom; i >= top; i --) {
            //result[resultIndex++] = matrix[i][left];
            result.add(matrix[i][left]);
        }
        //上移边界值:向上移动完毕后,判断是否执行从右向左移动
        if(left ++ == right ) break;
    }
    return result;
}

在这里插入图片描述

总结 数组篇的题型解题思路

大多数数组的问题,都可以采用穷举的思路,也就是暴力破解。
在本章节中
两数之和,我们采用两次循环,将所有的元素组合都列举出来,然后从所有的结果中取最大值。

针对数组篇,做算法优化,常见的有:
1、hash优化,减少一层循环;
2、双指针移动,过滤无效循环

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值