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)新开数组空间换时间的解法:

    a.遍历数组,如果是奇数从头部放入到原数组中,并记录指针
    b.如果是偶数,放入到新数组中,并记录指针
    c.将新数组的元素安顺序,从最后一个奇数后边插入到原数组中

  

原文地址:https://www.cnblogs.com/GuoXinxin/p/10441992.html