数组的基础题
数组拷贝
将 int[] arr1 = {10,20,30};拷贝到 arr2 数组, , 要求重新生成一份内存空间即它们的数据空间是独立的
int[] arr1 = {10,20,30};
//创建一个新的数组 arr2,开辟新的数据空间
int[] arr2 = new int[arr1.length];
//遍历 arr1 ,把每个元素拷贝到 arr2 对应的元素位置
for(int i = 0; i < arr1.length; i++) {
arr2[i] = arr1[i];
}
//修改 arr2, 不会对 arr1 有影响
arr2[0] = 100;
//输出 arr1
System.out.println("====arr1 的元素====");
for(int i = 0; i < arr1.length; i++) {
System.out.println(arr1[i]);//10,20,30
}
//输出 arr2
System.out.println("====arr2 的元素====");
for(int i = 0; i < arr2.length; i++) {
System.out.println(arr2[i]);//100,20,30
}
数组反转
通过找规律把arr数组的元素内容反转 {11,22,33,44,55,66} —>{66, 55,44,33,22,11}
- 把 arr[0] 和 arr[5] 进行交换 {66,22,33,44,55,11}*
- 把 arr[1] 和 arr[4] 进行交换 {66,55,33,44,22,11}
- 把 arr[2] 和 arr[3] 进行交换 {66,55,44,33,22,11}
- 一共要交换 3 次 = arr.length / 2
- 每次交换时,对应的两个下标是 arr[i] 和 arr[arr.length - 1 -i]
//定义一个临时变量
int temp = 0;
//计算数组的长度
int len = arr.length;
for( int i = 0; i < len / 2; i++) {
//交换
temp = arr[len - 1 - i];
arr[len - 1 - i] = arr[i];
arr[i] = temp;
}
System.out.println("===翻转后数组===");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");//66,55,44,33,22,11
}
使用逆序赋值方式完成数组内容的反转
- 先创建一个新的数组 arr2 ,大小 arr.length
- 逆序遍历 arr ,将每个元素拷贝到 新数组 arr2 的元素中(顺序拷贝)
- 循环拷贝的过程中建议增加一个循环变量 j -> 0 ->
- 当 for 循环结束,arr2 就是一个逆序的数组
- 让 arr 指向 arr2 数据空间, 此时 arr 原来的数据空间就没有变量引用会被当做垃圾,销毁
int[] arr = {11, 22, 33, 44, 55, 66};
int[] arr2 = new int[arr.length];
//逆序遍历 arr,增加一个循环变量 j -> 0 -> 5
for(int i = arr.length - 1, j = 0; i >= 0; i--, j++) {
arr2[j] = arr[i];
}
//让 arr 指向 arr2 数据空间
arr = arr2;
//输出arr
System.out.println("====arr 的元素情况=====");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
数组扩容
实现动态的给数组添加元素效果,并且添加完后用户可以决定是否继续添加
- 定义初始数组 int[] arr = {1,2,3} , 数组下标范围是 0-2 , 不能使用arr[3]
- 定义一个新的数组 int[] arrNew = new int[arr.length+1];
- 遍历 arr 数组,依次将 arr 的元素拷贝到 arrNew 数组
- 将新添加的元素赋给arrNew 的最后一个元素 arrNew[arrNew.length - 1]
- 让 arr 指向 arrNew 那么 原来 arr 数组就被销毁
- 创建一个 Scanner 可以接受用户输入
- 因为用户什么时候退出,不确定,所以使用 do-while + break 来控制
Scanner myScanner = new Scanner(System.in);
//初始化数组
int[] arr = {1,2,3};
do {
//创建一个新的数组
int[] arrNew = new int[arr.length + 1];
//遍历 arr 数组,依次将 arr 的元素拷贝到 arrNew 数组
for(int i = 0; i < arr.length; i++) {
arrNew[i] = arr[i];
}
//添加元素
System.out.println("请输入你要添加的元素");
int addNum = myScanner.nextInt();
//把 addNum 赋给 arrNew 最后一个元素
arrNew[arrNew.length - 1] = addNum;
//让 arr 指向 arrNew
arr = arrNew;
//输出 arr 看看效果
System.out.println("====arr 扩容后元素情况====");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
//询问用户是否继续添加
System.out.println("是否继续添加 y/n");
char key = myScanner.next().charAt(0);
//如果输入 n ,就结束
if( key == 'n') {
break;
}
}while(true);
System.out.println("你退出了添加...");
实现动态的对数组进行缩减,提示用户是否继续缩减,每次缩减最后那个元素。当只剩下最后一个元素,提示不能再缩减
Scanner myScanner = new Scanner(System.in);
//初始化数组
int[] arr = {1,2,3,4,5};
do {
if(arr.length > 1) {
//创建一个新的数组
int[] arrNew = new int[arr.length - 1];
//遍历 arr 数组,依次将 arr 的元素拷贝到 arrNew 数组
for(int i = 0; i < arrNew.length; i++) {
arrNew[i] = arr[i];
}
//让 arr 指向 arrNew
arr = arrNew;
//输出 arr 看看效果
System.out.println("====arr 扩容后元素情况====");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
//询问用户是否继续添加
System.out.println("是否继续缩减 y/n");
char key = myScanner.next().charAt(0);
//如果输入 n ,就结束
if( key == 'n') {
break;
}
} else{
System.out.println("只剩下一个元素...");
break;
}
}while(true);
System.out.println("你退出了添加...");
打印数组元素
创建一个 char 类型的 26 个元素的数组,分别 放置’A’-‘Z’。使用 for 循环访问所有元素并打印出来。提示:char 类型数据运算 ‘A’+2 -> 'C
- 定义一个 数组 char[] chars = new char[26]
- 因为 ‘A’ + 1 = ‘B’ 类推,所以使用 for 来赋值
- 使用 for 循环访问所有元素
char[] chars = new char[26];
for( int i = 0; i < chars.length; i++) {//循环 26 次
//chars 是 char[]类型
//chars[i] 是 char类型
//'A' + i 是 int , 需要强制转换
chars[i] = (char)('A' + i);
}
//循环输出
System.out.println("===chars 数组===");
for( int i = 0; i < chars.length; i++) {//循环 26 次
System.out.print(chars[i] + " ");
}
杨辉三角
使用二维数组打印一个10行杨辉三角
- 第一行有一个元素,第n行有n个元素,每一行的第一个元素和最后一个元素都是
- 从第三行开始,对于非第一个元素和最后一个元素的值 arr[i] [j] = arr[i-1] [j] + arr[i -1] [j-1]
1
1 1
1 2 1
1 3 3 1
int[][] yangHui = new int[12][];
//遍历 yangHui 的每个元素
for(int i = 0; i < yangHui.length; i++) {
//给每个一维数组(行)开空间
yangHui[i] = new int[i+1];
//给每个一维数组(行)赋值
for(int j = 0; j < yangHui[i].length; j++){
//每一行的第一个元素和最后一个元素都是1
if(j == 0 || j == yangHui[i].length - 1) {
yangHui[i][j] = 1;
} else {//中间的元素
yangHui[i][j] = yangHui[i-1][j] + yangHui[i-1][j-1];
}
}
}
//输出杨辉三角
for(int i = 0; i < yangHui.length; i++) {
for(int j = 0; j < yangHui[i].length; j++) {//遍历输出该行
System.out.print(yangHui[i][j] + "\t");
}
System.out.println();
}
插入元素
已知有个升序的数组 , 要求插入一个元素 , 该数组顺序依然是升序
数组扩容 + 定位 (可以冒泡但是效率不高)
先确定添加的数应该插入到哪个索引 , 然后扩容
- 遍历arr数组 , 如果发现 insertNum<=arr[i] , 说明 i 就是要插入的位置
- 使用index 保留添加数字的插入位置
- 如果遍历完后 , 没有发现 insertNum<=arr[i] , 说明 index = arr.length (即添加到arr的最后)
//定义原数组
int[] arr = {10,12,45,90}
//要插入的数字
int insertNum = 23;
//使用index , 保留添加数字的插入位置
int index = -1;
//遍历arr数组
for(int i = 0 ; i < arr.length; i++) {
if(insertNum <= arr[i]) {
index = i;
//找到位置后必须马上退出 , 否则插入的位置会出错
break;
}
if(index == -1){
//程序走到这里 , 说明还没有找到要添加的位置
index = arr.length;
}
}
//创建一个新的数组
int[] arrNew = new int[arr.length + 1];
//i 控制arrNew数组的下标 , j 控制arr数组的下标
for(int i = 0 , j = 0; i < arrNew.length; i++) {
//可以把arr的元素拷贝到 arrNew
if(i != index) {
arrNew[i] = arr[j];
//只有当拷贝了arr数组中的元素到arrNew时才会执行j++
j++;
}else {
//arrNew只要拷贝到了元素就会执行i++
arrNew[i] = insertNum;
}
}
//让 arr 指向 arrNew
arr = arrNew;
//输出 arr 看看效果
System.out.println("====arr插入数据后元素情况====");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
双指针法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MAKYqVAq-1677514268507)(C:\Users\meng\AppData\Roaming\Typora\typora-user-images\1674549245380.png)]
移除元素
给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于val的元素,并返回移除后数组的新长度。你不需要考虑数组中超出新长度后面的元素
- 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变
- 示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4
暴力的解法: 两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组
//时间复杂度:O(n^2)
//空间复杂度:O(1)
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
// 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位 , 表示重新在当前位置往后遍历
i--;
// 此时数组的大小-1
size--;
}
}
return size;
}
双指针法(快慢指针法)没有改变元素的相对位置: 两个指针起点相同,只不过一个走的快一个走的慢(满足一定条件)
- 快指针:遍历原数组寻找新数组的元素 ,快指针会一直更新直到遍历完原数组
- 慢指针:慢指针的更新取决于快指针指向的元素, 当快指针指向的元素不是待删除的元素时, 快指针才会把其指向的元素赋值给慢指针并更新慢指针
//时间复杂度:O(n)
//空间复杂度:O(1)
class Solution {
public int removeElement(int[] nums, int val) {
// 快慢指针
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
}
相向双指针法: 基于元素顺序可以改变的题目描述, 改变了元素相对位置,确保了移动最少元素
//时间复杂度:O(n)
//空间复杂度:O(1)
class Solution {
public int removeElement(int[] nums, int val) {
int leftIndex = 0;
int rightIndex = nums.length - 1;
while (leftIndex <= rightIndex) {
// 找左边第一个等于val的元素
while (leftIndex <= rightIndex && nums[leftIndex] != val){
++leftIndex;
}
// 找右边第一个不等于val的元素
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
-- rightIndex;
}
// 因为在循环内leftIndex或者rightIndex可能一直变化 , 覆盖前需要判断一下
// 将右边不等于val的元素覆盖左边等于val的元素
if (leftIndex < rightIndex) {
nums[leftIndex++] = nums[rightIndex--];
}
}
// leftIndex一定指向了最终数组末尾的下一个元素 , rightIndex小于leftIndex
return leftIndex;
}
}
有序数组的平方
给你一个按非递减顺序排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序
暴力排序
//时间复杂度O(n + nlogn)即O(nlogn)
vector<int> sortedSquares(vector<int>& A) {
for (int i = 0; i < A.size(); i++) {
A[i] *= A[i];
}
// 快速排序
Arrays.sort(A);
return A;
}
双指针法: 数组是有序的,那么数组元素平方的最大值一定在数组的两端,不是最左边就是最右边不可能是中间,从指针两边开始往中间移动
- left指向起始位置,right指向终止位置 , 比较这两个指针指向元素的平方的大小 , 将大的数先放入新数组并再移动其对应的指针
- 定义一个原数组一样的大小的新数组result,定义一个下标指向result数组的终止位置
//时间复杂度O(n)
class Solution {
public int[] sortedSquares(int[] nums) {
int right = nums.length - 1;
int left = 0;
int[] result = new int[nums.length];
int index = result.length - 1;
while (left <= right) {
if (nums[left] * nums[left] > nums[right] * nums[right]) {
// 正数的相对位置是不变的, 需要调整的是负数平方后的相对位置
result[index--] = nums[left] * nums[left];
++left;
} else { //nums[left] * nums[left] <= nums[right] * nums[right]
result[index--] = nums[right] * nums[right];
--right;
}
}
return result;
}
}
长度最小的子数组
给定一个含n个正整数的数组和一个正整数 s ,找出数组元素和 ≥ s 的长度最小的连续子数组并返回其长度。若没有符合条件的子数组返回 0
- 输入:s = 7, nums = [2,3,1,2,4,3] 输出:2 , 解释:子数组 [4,3] 是该条件下的长度最小的子数组
暴力解法: 一个for循环为滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,不断寻找符合条件的子序列
//时间复杂度:O(n^2)
//空间复杂度:O(1)
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
滑动窗口: 用一个for循环根据当前子序列和大小的情况,不断调节子序列的起始位置
- 窗口: 满足其和 ≥ s 的长度最小的连续子数组
- 窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(缩小窗口的值), 每缩小一次与s的进行比较一次
- 窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引
//时间复杂度:O(n)
//空间复杂度:O(1)
//不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数
//每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)
class Solution {
public int minSubArrayLen(int s, int[] nums) {
// 滑动窗口起始位置
int left = 0;
// 滑动窗口数值之和
int sum = 0;
// 滑动窗口的长度
int result = Integer.MAX_VALUE;
// 滑动窗口结束位置
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
// 注意这里使用while,每次更新 left(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
// 取子序列的长度
result = Math.min(result, right - left + 1);
// 这里体现出滑动窗口的精髓之处,缩小窗口的值不断变更left(子序列的起始位置)
sum -= nums[left++];
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == Integer.MAX_VALUE ? 0 : result;
}
}
模拟行为螺旋矩阵
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
#n=3(奇数)
1 2 3
8 9 4
7 6 5
#n=4(偶数)
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
坚持循环不变量原则 , 对每条边都是以左闭右开的原则处理 , 由外向内一圈一圈这么画下去
class Solution {
public int[][] generateMatrix(int n) {
// 控制循环次数同时控制每一圈里每一条边遍历的长度,每次循环右边界收缩一位
int loop = 0;
int[][] res = new int[n][n];
// 定义每循环一个圈的起始位置(start, start)
int start = 0;
// 定义填充矩阵的数字
int count = 1;
int i, j;
//例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
//loop从1开始
while (loop++ < n / 2) { // 判断边界后,
// 模拟填充上行从左到右(左闭右开)
for (j = start; j < n - loop; j++) {
res[start][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = start; i < n - loop; i++) {
//此时j == n - loop
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j >= loop; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i >= loop; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
start++;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
// 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
if (n % 2 == 1) {
res[start][start] = count;
}
return res;
}
}
找无序数组中个数超过一半的数字
数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字
对数组全部进行排序后,直接输出数组中间元素的值
//时间复杂度Nlogn
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.NextInt();
int[] arr = new int[n];
for(int i=0;i<arr.length;i++){
arr[i] = sc.NextInt();
}
Arrays.Sort(arr);
System.out.println(arr[arr.length/2]);
}
消除法: 两个数不同就消掉
//时间复杂度O(N)
static int f(int[] arr){
//侯选数
int Val = arr[0];
//出现的次数
int count = 1;
//从第二个元素开始扫描数组
for(int i = 1;i<arr.length;i++){
//两两消减为0,应该把现在的元素作为候选值
if(count==0){
Val = arr[i];
count = 1;
continue;
}
//遇到和候选数相同的次数加1
if(Val == arr[i]){
count++;
}else{ //遇到和候选数不同的进行消减
count--;
}
}
return Val;
}
顺序统计: 确定主元的位置 , 保证左右两边有序
- 数组中第N/2小的数一定是那个超过一半的数
- 也可以用hash统计
//时间复杂度O(N)
寻找发帖水王
如果你有一个论坛上所有帖子(包括回帖)的列表,其中帖子作者的ID也在表中,快速找出发帖数目超过了帖子总数的一半的作者
如果出现的次数 , 恰好为总个数的一半
- 水王占总数的一半 , 说明总数必为偶数 , 极限情况下最后消减完count为0 , 如{5,5,2,1}或 {5,2,5,1}
- 每次扫描的时候 , 多一个和最后一个元素比较并单独计数的动作 , 如果计数恰好为一半说明水王是最后一个元素 , 如果计数不足一半 说明水王不是最后一个元素,而是留下的那个侯选数 (只有count为0时侯选数才会变化)
static int f(int[] arr){
int Val = arr[0];
int count = 1;
//首先判断第一个元素和最后一个元素, indexlast用来统计和最后一个元素相等的次数(也可以在循环内判断,从0开始),判断最后一个元素是不是水王
int indexlast = arr[0]==arr[arr.length-1]?1:0;
for(int i=1;i<arr.length;i++){
//从第二个元素开始增加和最后一个元素比较的步骤
if(arr[i]==arr[arr.length-1]){
indexlast++;
}
if(count==0){
Val = arr[i];
count = 1;
continue;
}
if(Val==arr[i]){
count++;
}else{
count--;
}
}
//最后一个元素出现N/2,最后一个元素是水王
if(indexlast==arr.length/2){
return arr[arr.length-1]
}else{//否则就是最后一个侯选数
return Val;
}
}
最小可用id
在非负数(乱序)中找到最小的可分配的id(从1开始连续编号),数据量1000000 , 在乱序数组中寻找那个空缺的数
暴力循环: 从1开始一次探测每个自然数是否在该数组中
//O(N^2)
static int f(int[] arr){
int i=1;
while(i<arr.length){
if(indexof(arr,i)==-1){
return i;
}
}
}
static int indexof(int[] arr,int k){
for(int i=0;i<arr.length;i++){
if(arr[i]==k)
return 1
}
return -1;
}
先排序,然后依次单向扫描数组, 判断每个数字是否在对应的下标
//排序O(nlogn),依次遍历O(N) , 取大的整体算法O(nlgn)
static int f(int[] arr){
Arrays.Sort(arr);
for(int i=0;i<arr.length;i++){
if(arr[i]!=i+1){
return i+1;
}
}
//扫描到最后一个元素都没有缺失的数,直接返回数组长度+1(i=arr.length)
return i+1;
}
创建辅助空间: 新建长为n+1的数组, 初始值全为0
- 扫描数组中的元素 , 元素的值是多少在辅助空间的下标就是多少 , 并把该下标处的值改为1
- 扫描新数组 , 找到第一个数值等于0的元素 , 即原数组缺失的数
//O(N)
static int f(int arr){
int[] help = new int[arr.length+1];
//扫描原数组,把数放在新数组中对应的下标
for(int i = 0;i<arr.length;i++){
//如果值大于等于数组的长度就每必要考虑放到新数组当中(没有对应下标),因为值都是连续编号的
if(arr[i]<help.length){
help[arr[i]] = 1
}
}
//从第二个元素开始扫描新数组,找到第一个没有赋值的元素
for(int i = 1;i < help.length;i++){
if(help[i]==0){
return i;
}
}
//新数组的所有元素都被赋值了
return help.length+1;
}
static int f(int arr){
int[] help = new int[arr.length];
for(int i = 0;i<arr.length;i++){
if(arr[i]<=help.length){
help[arr[i]-1] = 1
}
}
for(int i = 0;i < help.length;i++){
if(help[i]==0){
return i+1;
}
}
return help.length+1;
}
区间分割法: 假设一个长度为100的数组分为以下三种情况
1.中间值恰好为50时,表明数组左半区间数字元素紧凑,没有要找的缺少元素
2.中间值为大于50的值,表明数组左半区间有漏掉的元素
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DdXzUc10-1677514268508)(C:\Users\meng\AppData\Roaming\Typora\typora-user-images\1674004436757.png)]
public class Smallidfenqu {
public static int find4(int[] arr,int l,int r) { //定义数组,左指针,右指针
if(l>r) return l+1;
//数组中间元素的下标,用右指针减去左指针加1
int midIndx=l+((r-l)>>1);
//找到数组中第midIndx -l + 1小的元素 ,也就是数组中间位置的值
int q=selectk(arr,l,r,midIndex-l+1);
//t为要求的期望值
int t=midIndex+1;
if(q==t) { //左侧紧密
return find4(arr,midIndex+1,r); //在右半区间查找
}
else { //左侧稀疏
return find4(arr,l,midIndex-1); //在左半区间查找
}
}
public static void main(String[] args) {
int[] arr= {1,2,3,4,5,8,9,10,11,10000};
arr=new int[1000*1000];
}
}
其他
54.螺旋矩阵
剑指Offer 29.顺时针打印矩阵
26.删除排序数组中的重复项
283.移动零
844.比较含退格的字符串
排序思想的应用
调整数组顺序使奇数位与偶数前面
输入一个整型数组,调整数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分,要求时间复杂度O(N)
快排单向扫描法的思想
- 左指针扫描到奇数则前进
- 左指针扫描到偶数则和右指针指向的元素交换 , 同时右指针向左前进一步 , 然后左指针继续往前扫描
//时间复杂度O(N)
static void f(int[] arr) {
int Scan = 0;
int os = arr.length-1;
while(Scan<=os) {
if(arr[Scan]%2!=0) {
Scan++;
}else {
util.swap(arr, Scan, os);
os--;
}
}
}
归并排序的思想
- 利用一个辅助空间 , 扫描到奇数则放到左指针指向的位置同时指针右移 , 扫描到偶数则放到右指针指向的位置同时指针左移
//时间复杂度O(N)
//空间复杂度O(N)
数组中第k小的数
以尽量高效率求出一个乱序数组中的按数值顺序的第k个元素值
暴力解法
- 先对乱序数组进行排序 , 然后找到第k个元素值 , 下标对应k-1 , 时间复杂度nlog2n
快排思想:需要改动数组的内容
- 找到主元的位置,主元的位置就表示主元是第几小的
- 要找的位置比主元位置大了去右边找 , 要找的位置比主元位置小了去左边找
//时间复杂度O(N) , 最差O(N方)
static int selectK(int[] arr,int begin,int end,int k) {
//先找到主元的下标,根据下标看主元是第几小的
int q = partition(arr,begin,end);
//第qk小
int qk = q+1;
if(qk>k) {//主元大于要找的往左找
return selectK(arr, begin, q-1, k);
}else if(qk<k) {//主元小于要找的往右找,右边是个新序列,要在新序列找第k-qk小的
return selectK(arr, q+1, end, k-qk);
}
return arr[q];
}
//找到主元的下标
static int partition(int[] arr,int begin ,int end) {
//三点确定法
int indexMid = (begin+end)>>1;
if(arr[begin]>=arr[indexMid] && arr[begin]<=arr[end]) {
indexMid = begin;
}else if(arr[end]>=arr[begin] && arr[end]<=arr[indexMid]) {
indexMid = end;
}
util.swap(arr, begin, indexMid);
//把三点确定法确定的值和第一个值交换,这样不影响
int poivt = arr[begin];
int left = begin+1;
int right = end;
while(left<=right) {
while(left<=right && arr[left]<=poivt)left++;
while(left<=right && arr[right]>poivt)right--;
if(left<right)
util.swap(arr, left, right);
}
util.swap(arr, begin, right);
return right;
}
合并有序数组
给定两个排序后的数组A和B,其中A的末端有足够的缓冲空间容纳B , 编写一个方法 , 将B合并入A并排序
归并排序的思想
- 在A数组中确定两个数组合并后最后一个元素的下标
- 在A,B两个数组中分别定义两个指针指向数组末尾的元素
逆序对
设 A为一个有n个各不相同数字的有序集(n>1), 如果存在正整数 i , j 使得 1≤ i < j ≤n 而且A[i] > A[j].则<A[i],A[j]>这个有序对称为A的一个逆序对(逆序数)
- 数组(3,1,4,5,2)有4个逆序(3,1),(3,2),(4,2),(5,2),满足下标小的的对应元素值大于下标大的对应的元素值
在归并排序的过程中 , 只要抓右侧的数,就有逆序对
同时遍历两个数组,对于遍历到的两个数,比较大小。如果arr1数组中的元素比arr2数组中的元素小,就把arr1中的当前元素放入到我们新生成的数组中去,arr1数组的指针往后移一下,反之亦然。
仔细想一下,我们在进行arr1和arr2数组比较的时候,肯定有一种情况是:arr2中的当前元素比arr1中的当前元素小,每当遇上这种情况就计算出arr1数组中当前位置到最后位置的元素的个数类加上,得到的累加和就是最后逆序对的个数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LlaBCxd6-1677514268509)(D:/%E7%AC%94%E8%AE%B0/%E7%AE%97%E6%B3%95%E9%A2%98/image/%E9%80%86%E5%BA%8F%E5%AF%B9.png)]
int niXu = 0;
int arr[] = { 8, 4, 5, 7, 1, 3, 6, 2 };
//归并排序需要一个额外空间
int[] helper = new int[arr.length];
mergeSort(arr, 0, arr.length - 1);
static void MergeSort(int[] arr,int begin,int end){
if(begin<end){
int indexMid = (begin+end)>>1;
//对左侧排序
MergeSort(arr,begin,indexMid);
//对右侧排序
MergeSort(arr,indexMid+1,end);
//合并
Merge(arr,begin,indexMid,end);
}
}
static void Merge(int[] arr,int begin,int Mid,int end){
//将原始数组的元素拷贝到辅助数组
System.arraycopy(arr, begin, helper, begin, (end-begin)+1);
//原始数组的指针(待覆盖数组)
int current = begin;
//辅助数组的两个指针
int left = begin;
int right = Mid + 1;
while(left<=Mid && right <= end){
if(helper[left]<=helper[right]){
arr[current++] = helper[left++];
}
else{//右边小
arr[current++] = helper[right++];
//只要右边下就加左边元素的个数
niXu+=mid-left+1;
}
}
//左边的有序序列还有剩余的元素,就全部填充到原始数组
//右边的有序序列还有剩余的元素没有关系,因为原始数组的剩余元素也是这些
while(left<=Mid){
arr[current++] = helper[left++];
}
}