【数据结构与算法(十八)】

1、图形能使很多复杂的抽象问题变得很形象,特别是对于链表、二叉树的问题
2、二叉树问题:关于遍历?关于排序?挺多都是用递归的方法解决的
3、很多抽象的题不能一下子得到解题的思路,那就举几个简单的例子,然后找出规律,重点在于找出规律,这样写出来的算法才不会冗杂
4、把复杂问题分解为若干个小问题,也是需要掌握的思想。
5、分治法和动态规划!!!!
6、关于时间复杂度和空间复杂度
7、关于传参: C++编程时,要习惯采用引用或指针**传递复杂类型参数的习惯。如果采用值传递参数的方式,则从形参到实参会产生一次复制操作。这样的复制操作明显是多余的,能免就免
8、关于递归:递归的本质是把一个大的复杂问题分解为两个或多个小的简单问题。如果小问题中有相互重叠的部分,那么直接用递归实现虽然代码显得很简洁,但你要是仔细的去步入那个递归的过程,会发现它的时间效率很差,之前有一道题练习过了【递归与循环—斐波那契数列】。对于这种问题,可以使用递归的思路来分析,但在写代码的时候可以用数组来保存中间结果基于循环实现。绝大部分的动态规划算法的分析和代码实现都是分这两个步骤完成的
9、关于常见的数据结构和算法:一定要掌握,而且还要会分析在什么时候选择什么样的数据结构和常见的算法(进行变形)才能使时间效率和空间效率尽量低(一般优先考虑时间效率)
10、对于问题,能先想到哪一种就用哪一种,但是要基于这个解决方法,想到更好的、时间效率更低的解决方法才行。

题目

字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。例如,输入字符串abc,则打印出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。

思路(递归,分治)

1、求所有可能出现在第一个位置的字符,即把第一个字符和后面所有的字符交换
2、固定第一个字符,求后面所有字符的排列。这时我们仍把后面的所有字符分为两部分:后面字符的第一个字符,以及这个字符之后的所有字符。然后把第一个字符逐一和它后面的字符交换。

#include<iostream>
using std::cout;

void Permutation(char* pStr)
{
    if (pStr == nullptr)
        return;
    Permutation(pStr, pStr);
}

void Permutation(char* pStr, char* pBegin)
{   //①递归退出的条件
    //②存在,但为空串“”,没有被赋值(默认)
    if (*pBegin == '\0')
        cout << pStr;
    else
        for (char* pCh = pBegin; *pCh != '\0'; pCh++) {
            char temp = *pCh;
            *pCh = *pBegin;
            *pBegin = temp;

            Permutation(pStr, pBegin + 1);
            //还原回来,准备与下一个字符交换,不然下一次与下一个字符交换的就不是原来那个*pBegin字符了
            temp = *pCh;
            *pCh = *pBegin;
            *pBegin = temp;
        }
}

3、相关题目:输入一个含有8个数字的数组,判断有没有可能把这8个数字分别放到正方体的8个顶点上,使得正方体上三组相对面上的四个顶点的和都相等。

bool PermutationInt(int* pInt,int length)
{
    if (pInt == nullptr || length!=8)
        return;
    return PermutationInt(pInt, pInt,1);
}
bool PermutationInt(int* pInt, int* pBegin,int order)
{
    bool isFix = false;
    if (order == 8)
        isFix = isOK(pInt);
    else
        for (int* pInt= pBegin,int pNum=order; pNum != 8; pNum++,pInt++) {
            int temp = *pInt;
            *pInt = *pBegin;
            *pBegin = temp;

            isFix=PermutationInt(pInt, pBegin + 1, pNum + 1);

            temp = *pInt;
            *pInt = *pBegin;
            *pBegin = temp;
        }
    return isFix;
}

bool isOK(int* pInt)
{
    return pInt[0] + pInt[1] + pInt[2] + pInt[3] == pInt[4] + pInt[5] + pInt[6] + pInt[7]
        && pInt[0] + pInt[1] + pInt[4] + pInt[5] == pInt[2] + pInt[3] + pInt[6] + pInt[7]
        && pInt[0] + pInt[2] + pInt[4] + pInt[7] == pInt[1] + pInt[3] + pInt[5] + pInt[6];
}

数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如,输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}.由于数字2出现了5次,超过数组长度的一半,因此输出2。

思路

解法1:基于Partition函数的时间复杂度为O(n)的算法

1、关键字:一半,也就是说数组的中位数(即长度为n的数组中第n/2个数)就是我们要找的数字了。这里有一个常用的时间复杂度为O(n)的算法得到数组中任意第k大的数字。
2、这种算法类似于快速排序算法。在随机快速排序算法,先在数组中随机选择一个数字,然后调整数组中数字的顺序,使得比选中的数字小的数字都排在它的左边,比选中的数字大的数字都排在它的右边。如果这个选中的数字的下标刚好是n/2,那么这个数字就是数组的中位数;如果选中的数字的下标大于n/2,那么中位数应该位于它的左边,我们可以接着它的左边部分的数组继续查找。如果小于n/2,就在它的右边部分的数组继续查找。这是一个递归问题。
快速排序:O(n)
3、对于传入的数组是以一个指针传入的时候,我们要考虑数组的指针是否存在(==nullptr),以及是否为一个空数组(长度为0)

#include<exception>
void Swap(int *x, int *y)
{
    int p;
    p = *x;
    *x = *y;
    *y = p;
}
int  Partition(int data[], int length, int start, int end)
{
    if (data == nullptr || start < 0 || end >= length || length <= 0)
        throw new std::exception("不合格的输入!");

    int small = start - 1;  //指向最小的数
    int flag = data[end];       //用于进行划分的标志
    for (int i = 0; i < length; i++)
    {
        if (data[i] < flag) {
            small++;
            Swap(&data[i], &data[small]);
        }
    }
    //最后还要将最后一个数放到前面,和其他小的数放到一起
    ++small;
    Swap(&flag, &data[small]);

    return small;   //返回的是最后一个较小的数的位置,即大数与小数划分处
}
//有可能并不存在出现次数超过一半的数组,所以要检查一下,这就是不合法的输入了!!!!
int CheckMoreThanHalf(int* numbers, int length, int number)
{
    int times = 0;
    for (int i = 0; i < length; i++) {
        if (numbers[i] == number)
            times++;
    }
    bool isMoreThanHalf = true;
    if (times * 2 <= length)
        isMoreThanHalf = false;
    return isMoreThanHalf;
}
//方法1:基于Partition函数的时间复杂度为O(n)的算法
int MoreThanHalfNum_1(int* numbers, int length)
{
    if (numbers == nullptr || length == 0)
        return 0;
    int middle = length >> 1;   //除以2
    int start = 0;
    int end = length - 1;
    int index = Partition(numbers, length, start, end);//分界处
    while (index != middle) {
        if (index > middle) {
            end = index - 1;
            index = Partition(numbers, length, start, end);
        }
        else {
            start = index + 1;
            index = Partition(numbers, length, start, end);
        }
    }
    int result=numbers[middle];
    if (!CheckMoreThanHalf(numbers, length, result))
        result = 0;
    return result;
}

不仅要解决问题,还要更好地解决问题

解法2:根据数组特点找出时间复杂度为O(n)的算法

4、从另外一个角度想问题。数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数的和还要多。所以我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字;另一个是次数。当我们遍历到下一个数字的时候,和我们之前保存的数字不同,则次数减1;如果次数为0,那么我们需要保存下一个数字,并把次数设为1。由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。这句话什么意思?想想具体例子中的,就是用一个其他数抵消一个出现次数超过数组长度一半的数,因为其他数的个数的总和都比目标数少,所以就抵不掉所有了。

//方法2:根据数组特点(就是说要审题)找出使劲按复杂度为O(n)的算法
int MoreThanHalfNum_2(int* numbers, int length)
{
    if (numbers == nullptr || length == 0)
        return 0;

    int result = numbers[0];
    int times = 1;
    for (int i = 1; i < length; i++) {
        if (times == 0) {
            result = numbers[i];
            times++;
        }
        else if (numbers[i] == result)
            times++;
        else times++;
    }
    if (!CheckMoreThanHalf(numbers, length, result))
        result = 0;
    return result;
}

解法1中使用了Partition函数,改变了数组,如果要求不能改变数组,那就只能用第二种做法了
时间复杂度,怎么分析?第一种解法的时间复杂度是基于Partition分析的,所以为什么是O(n)?

最小的k个数

输入n个整数,找出其中最小的k个数。例如,输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4

思路

1、最简单的思路:将n个数排序,之后将位于前面的k个数输出就可以了。时间复杂度O(n logn)

解法1:时间复杂度O(n)的算法,只有当我们可以修改输入的数组时可用

2、还是使用Partition函数来解决。如果基于数组的第k个数字来调整,则使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的所有数字都位于数组的右边。这样调整之后,位于数组中左边的k个数字就是最小的k个数字(不一定排序)

//基于Partition函数
void GetLeastNumbers(int* input, int n, int* output, int k)
{
    //为什么output也判断?因为如果output没有指向
    //就是说它根本不存在的话,返回的输出要放到哪个地址都不知道
    if (input == nullptr || output == nullptr || k > n || n <= 0 || k <= 0)
        return;
    int start = 0;
    int end = n - 1;
    int index = Partition(input, n, start, end);
    while (index != k - 1) {
        if (index > k - 1) {
            end = index - 1;
            index = Partition(input, n.start, end);
        }
        else {
            start = index + 1;
            index = Partition(input, n, start, end);
        }
    }
    for (int i = 0; i < k; i++)
        output[i] = input[i];
}

解法2:时间复杂度为O(n logk)的算法,特别适合处理海量数据

3、创建一个大小为k的数据容器来存储最小的k个数字,接下来每次从输入的n个整数中读入一个数。
4、如果容器中已有的数字少于k个,则直接把这次读入的整数放入容器之中;如果容器中已有k个数字,也就是说容器已满,此时我们不能再插入新的数字而只能替换已有的数字。找出容器k个数中的最大值,然后拿此次待插入的整数和最大值进行比较。如果待插入的值比当前已有的最大值小,则用这个数替换当前已有的最大值;如果待插入的值比当前已有的最大值还要大,那么这个数不可能是最小的k个整数之一,所以就可以抛弃这个整数了
5、当容器满了之后需要做的事:①在k个整数中找到最大值;②有可能在这个容器中删除最大数;③插入一个新的数字。如果在一棵二叉树中实现这个数据容器,那么我们能在O(log k)的时间内完成这3个操作。
6、选择用不同的二叉树来实现这个数据容器—使用最大堆或者红黑树。最大堆的操作可能需要比较多的代码,所以使用红黑树【什么是红黑树?先用着】。红黑树通过把节点分为红、黑两个颜色并根据一些规则确保树在一定程度上是平衡的,从而保证在红黑树中的查找、删除和插入操作都只需要O(log k)时间。在STL中,set和multiset都是基于红黑树实现的。直接拿来用

typedef multiset<int, greater<int>> intSet;
typedef multiset<int, greater<int>>::iterator setIterator;

//解法2:使用红黑树
void GetLeastNumbers_2(const vector<int>&data, intSet& leastNumbers, int k)
{
    leastNumbers.clear;
    if (k < 1 || data.size() < k)
        return;

    vector<int>::const_iterator iter = data.begin();
    for (; iter != data.end(); iter++) {
        if (leastNumbers.size() < k)
            leastNumbers.insert(*iter);
        else {
            setIterator iterGreatest = leastNumbers.begin();
            if (*iter < *(leastNumbers.begin())) {
                leastNumbers.erase(iterGreatest);
                leastNumbers.insert(*iter);
            }
        }
    }
}
点赞