21 调整数组的顺序使奇数位于偶数的前面 (第3章 高质量的代码-代码的完整性)
题目描述:(题目一)
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。
测试用例:
功能测试:(输入的数组中的奇数、偶数交替出现;所有偶数都在奇数的前面;输入的数组中所有奇数都出现在偶数的前面)
特殊输入测试:(输入为nullptr;输入的数组只包含一个数字)
解题思路:
1) 初级程序员:维护两个指针,一个指向数组的第一个数字,一个指向数组的最后一个数字;两者向中间移动,第一个指针寻找偶数,第二个指针寻找奇数,将两者内容交换。(快速排序算法)
class Solution { public: void reOrderArray(vector<int> &array) { if(array.empty()) return; int front = 0; int back = array.size()-1; while(front<back){ //寻找偶数 while(front<back && (array[front]&0x1)!=0)//不是偶数时,寻找下一个 error:(array[front]&0x1!=0) 因为!=优先级高于& ++front; //寻找奇数 while(front<back && (array[back]&0x1)!=1) //不是奇数时,寻找下一个 --back; if(front<back){ //交换 array[front] = array[front] + array[back]; array[back] = array[front] - array[back]; array[front] = array[front] - array[back]; //++front; //--back; } } } };
注意:!=优先级高于& 因此判断是要对(array[front]&0x1)加括号,否则在寻找奇数时,会先计算0x1 !=1 永远不成立,因此永远不会进入到循环内
2)高级程序员:写成一些列同类型的通用写法
-- 题目改成把数组中的数按照大小分为两部分,所有负数都在非负数的前面。
-- 能被3整除的数都在不能被3整除的数的前面。
上述问题都只要修改第二个while跟第三个while的的判断条件即可
通用的解决方法:把判断的标准变成一个函数指针,也就是用一个单独的函数来判断数字是否符合标准。
class Solution {
public:
void reOrderArray(vector<int> &array, bool (*func)(int)) {
if(array.empty())
return;
int front = 0;
int back = array.size()-1;
while(front<back){
//寻找偶数
while(front<back && !func(array[front]) )//不是偶数时,寻找下一个 error:(array[front]&0x1!=0) 因为!=优先级高于&
++front;
//寻找奇数
while(front<back && func(array[front]) ) //不是奇数时,寻找下一个
--back;
if(front<back){
//交换
array[front] = array[front] + array[back];
array[back] = array[front] - array[back];
array[front] = array[front] - array[back];
//++front;
//--back;
}
}
}
bool isEven(int n){
return (n&1)==0; //偶数返回true
}
};
//调用函数方法:
reOrderArray( array, isEven); //第二个参数,可以输入别的判断函数
题目描述:(题目二)
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。并保证奇数和奇数,偶数和偶数之间的相对位置不变。
测试用例:
功能测试:(输入的数组中的奇数、偶数交替出现;所有偶数都在奇数的前面;输入的数组中所有奇数都出现在偶数的前面)
特殊输入测试:(输入为nullptr;输入的数组只包含一个数字)
解题思路:
1)使用vector内置的函数erase与push_back,从左到右遍历,当遇到偶数时,将其在原位置删除并在vector的后面追加。
class Solution { public: void reOrderArray(vector<int> &array) { //if(array.empty()) //return; vector<int>::iterator even = array.begin(); int size = array.size(); while (size) { if ((*even &01) == 0) { int tmp = *even; even = array.erase(even); //使用erase函数 array.push_back(tmp); }else{ even++; } size--; } } };
注意:该方法的效率主要取决于erase的效率。
erase()函数删除当期位置,然后将后面的数据前移 效率较低。
2)类似冒泡算法,前偶后奇数就交换:
class Solution { public: void reOrderArray(vector<int> &array) { for (int i = 0; i < array.size();i++) //每一次i循环,可以确定i位置的奇数,离i最近的奇数会传递到i位置 { for (int j = array.size() - 1; j>i;j--)//j每次都从尾部遍历到i后面 { if ((array[j] & 0x1) == 1 && (array[j - 1] & 0x1) == 0) //前偶后奇交换 { //交换两个值swap(array[j], array[j-1]); array[j] = array[j] + array[j-1]; array[j-1] = array[j] - array[j-1]; array[j] = array[j] - array[j-1]; } } } } };
3)使用stable_partition函数:
//实现1 判断函数定义在类外部
bool isOk(int n){ return ((n & 1) == 1); //奇数返回真 } class Solution { public: void reOrderArray(vector<int> &array) { stable_partition(array.begin(),array.end(),isOk); } };
//实现2 判断函数定义在类内部
class Solution { public: void reOrderArray(vector<int> &array) { stable_partition(array.begin(),array.end(),isOk); } static bool isOk(int n){ //定义在类内,要加static,否则类内会报错。与*this指针有关 return ((n & 1) == 1); //奇数返回真 } };
//实现3 lambda表达式
class Solution { public: void reOrderArray(vector<int> &array) { stable_partition(array.begin(), array.end(), [](int i){return (i & 1) == 1;}); //奇数返回真 } };
注:
[1] stable_partiton(first,last,compare);//first为容器的首迭代器,last为容器的末迭代器,compare为比较函数(可略写)。1 2 3 4 5 序列,用partition函数 那么输出不一定是:1 3 5 2 4 也有可能是:3 5 1 4 2。如果使用stable_partition函数(稳定整理算法),那么输出一定就是:1 3 5 2 4
[2] stable_partition函数源码其实是开辟了新的空间,然后把符合条件的放在新空间的前面,其他的放在后面。 代码中看起来时在原array上进行的操作
[3] 可以向一个算法传递任何类别的可调用对象。可调用对象有函数,函数指针,其实还有重载了函数调用运算符的类,和lambda表达式。
-- lambda表达式 : 用于定义并创建匿名的函数对象,以简化编程工作。
-- 语法形式:
[capture list] (params list) mutable exception-> return type { function body } 各项具体含义如下 1. capture list:捕获外部变量列表 2. params list:形参列表 3. mutable指示符:用来说用是否可以修改捕获的变量 4. exception:异常设定 5. return type:返回类型 6. function body:函数体 可以省略其中的某些成分来声明“不完整”的Lambda表达式,常见的有以下几种 //省略 mutable指示符、exception:异常设定 1. [capture list] (params list) -> return type {function body} 格式1声明了const类型的表达式,这种类型的表达式不能修改捕获列表中的值 //省略返回类型及mutable指示符、exception:异常设定 2. [capture list] (params list) {function body} 格式2省略了返回值类型,但编译器可以根据以下规则推断出Lambda表达式的返回类型: (1):如果function body中存在return语句,则该Lambda表达式的返回类型由return语句的返回类型确定; (2):如果function body中没有return语句,则返回值为void类型。 3. [capture list] {function body} 格式3中省略了参数列表,类似普通函数中的无参函数。 示例: [] (int x, int y) { return x + y; } // 隐式返回类型 [] (int& x) { ++x; } // 没有 return 语句 -> Lambda 函数的返回类型是 'void' [] () { ++global_x; } // 没有参数,仅访问某个全局变量 [] { ++global_x; } // 与上一个相同,省略了 (操作符重载函数参数) //显示指定返回类型 [] (int x, int y) -> int { int z = x + y; return z; }
4)新建数组,遍历两遍,先存奇数,再存偶数
class Solution { public: void reOrderArray(vector<int> &array) { vector<int> res; //存奇数 for(int i = 0; i < array.size(); i++) { if((array[i] & 1) == 1) res.push_back(array[i]); } //存偶数 for(int i = 0; i < array.size(); i++) { if(array[i] % 2 == 0) res.push_back(array[i]); } //array.swap(res); //swap的效率? array = res; //或者用 swap }
注:以后判断奇偶性,不要用 array[i] % 2 == 1 因为不仅效率低,而且当array[i] 为负数,比如-1%2=-1 并不等于1。
用空间换时间,存偶数与存奇数分别是O(n),最后array=res;的操作,时间复杂度是O(1)么?
5)插入排序算法
相对位置不变--->保持稳定性;奇数位于前面,偶数位于后面 --->存在判断,挪动元素位置;
//java public class Solution { public void reOrderArray(int [] array) { //相对位置不变,稳定性 //插入排序的思想 int m = array.length; int k = 0;//记录已经摆好位置的奇数的个数 for (int i = 0; i < m; i++) { //将找到的奇数移动到已确定的k的后面,k++,可以确定k+1个 if (array[i] % 2 == 1) { int j = i; while (j > k) {//j >= k+1 int tmp = array[j]; array[j] = array[j-1]; array[j-1] = tmp; j--; } k++; } } } }
6)归并排序 (快速排序是不稳定的,这里不能使用)归并时间复杂度是nlogn,需要额外的O(N)空间
//java 该算法过于麻烦 待考虑 public class Solution { public void reOrderArray(int [] array) { //用用归并排序的思想,因为归并排序是稳定的 int length = array.length; if(length == 0){ return; } int[] des = new int[length]; MergeMethod(array, des, 0,length - 1); } public void MergeMethod(int[] array, int [] des, int start, int end){ if(start < end){ int mid = (start + end) / 2; MergeMethod(array, des, start, mid); //前半部分 MergeMethod(array, des, mid + 1, end); //后半部分 Merge(array, des, start, mid, end); } } public void Merge(int[] array, int[] des, int start, int mid, int end){ int i = start; int j = mid + 1; int k = start; while(i <= mid && array[i] % 2 == 1){ des[k++] = array[i++]; } while(j <= end && array[j] % 2 == 1){ des[k++] = array[j++]; } while(i <= mid){ des[k++] = array[i++]; } while(j <= end){ des[k++] = array[j++]; } for(int m = start; m <= end; m++){ array[m] = des[m]; } } }
7)新开数组空间换时间的解法: