剑指offer 学习笔记 查找和排序

快排:先在数组中选择一个数字,接下来把数组中的数字分为两部分,比选择的数字小的数字移到数组的左边,比选择的数字大的数字移到数组的右边:

#include <iostream>
using namespace std;

int Partition(int data[], int length, int start, int end) { 
    if (data == nullptr || length <= 0 || start < 0 || end >= length) {
        throw new exception("Invalid Parameters");
    }

    int index = (rand() % (end - start + 1)) + start;    // 在当前排序的数列中找到一个随机基准数,rand返回0~RAND_MAX之间的数

    int temp = 0;    // 将基准数与最后一个数字交换,将其放在最后位置上
    temp = data[end];
    data[end] = data[index];
    data[index] = temp;

    int small = start - 1;    // small作用为:包括small在内的左侧数都比基准数小,右侧的数都比基准数大
    for (index = start; index < end; ++index) {    // 遍历要排序的数列
        if (data[index] < data[end]) {    // 当index指向的数比基准数小时,就要将index指向的数放到small的左闭区间内
            ++small;    // 由于index是从左到右依次遍历的,因此可以保证此时small指向的数一定大于等于index指向的数,等号只在index和small相等时取到
            if (small != index) {    // 如果small与index相等,就不用交换顺序了,否则就要交换
                temp = data[index];    
                data[index] = data[small];
                data[small] = temp;    // 此时比基准数小的数被放到了small的左闭区间内
            }
        }
    }
                // 以上循环结束后,small的左闭区间内的数都比基准数小,右侧的数都比基准数大
    ++small;    // 将small指向数列中第一个比基准数大的数的位置
    temp = data[end];    // 交换small指向的数与基准数
    data[end] = data[small];
    data[small] = temp;

    return small;    // 此时small左侧数都比其小,右侧数都比其大
}

void QuickSort(int data[], int length, int start, int end) {
    if (start == end) {
        return;
    }

    int index = Partition(data, length, start, end);    // 找到数列基准点并将比基准点小的数放到左侧,比基准数大的数放到右侧
    if (index > start) {
        QuickSort(data, length, start, index - 1);    // 对基准数左侧快排
    }
    if (index < end) {
        QuickSort(data, length, index + 1, end);    // 对基准数右侧快排
    }
}

int main() {
    int data[] = { 7,8,5,4,0,1,2,9,3,6 };
    QuickSort(data, 10, 0, 9);
    for (int i : data) {
        cout << i << endl;
    }
    return 0;
}

快排的平均效率是最好的,但也不是任何时候都是最优算法,如数组本身已经排好序了,而每一轮都以最后一个数字作为比较的标准时,效率只有O(n²)。

选择适合的排序方法,如对公司中员工年龄进行排序,年龄范围为0~99,人数是一个较小的数据量,时间效率为O(n),所用的辅助空间为常数级:

#include <iostream>
using namespace std;

void SortAges(int ages[], int length) {
    if (ages == nullptr || length <= 0) {
        return;
    }

    const int oldestAge = 99;
    int timesOfAge[oldestAge + 1];    // 存放每个年龄出现的次数
    for (int i = 0; i < oldestAge + 1; ++i) {    // 初始化
        timesOfAge[i] = 0;
    }

    for (int i = 0; i < length; ++i) {    // 统计每个年龄出现的次数
        ++timesOfAge[ages[i]];
    }

    int index = 0;
    // 将每个年龄出现的次数展开即可完成排序
    for (int i = 0; i < oldestAge + 1; ++i) {
        for (int j = 0; j < timesOfAge[i]; ++j) {
            ages[index] = i;
            ++index;
        }
    }
}

int main() {
    int ages[] = { 7,8,5,4,0,1,2,9,3,3,6 };
    SortAges(ages, 10);
    for (int i : ages) {
        cout << i << endl;
    }
    return 0;
}

面试题11:旋转数组的最小数字。把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。如,输入的递增排序数组的一个旋转为{3,4,5,1,2},为{1,2,3,4,5}的一个旋转,该数组的最小值为1。

直观上,只需遍历一遍数组,总能找到最小值,时间复杂度为O(n),但没有利用旋转数组的特性。

我们注意到旋转之后的数组实际上可以划分为两个排序的子数组,而且前面子数组的元素都大于或者等于后面子数组的元素,并且最小的元素刚好是这两个子数组的分界线。在排序的数组中我们可以使用二分法实现O(logn)的查找,本题中数组一定程度上是有序的,我们可以试着用二分法处理。我们用两个指针分别指向数组的第一个元素和最后一个元素,第一个元素应该是大于等于最后一个元素的(不一定对,后面讨论特例)。接着我们可以找到数组中间的元素,如果该元素位于前面的递增数组中,那么它的值应该大于等于第一个指针指向的元素,此时最小的元素应该位于该中间元素的后面,我们可以把第一个指针指向该中间元素,这样就缩小了范围,移动后第一个指针仍然位于前面的递增子数组;同样,如果中间指针指向的元素位于后面的递增子数组,它应该小于等于第二个指针指向的元素,此时最小的元素位于该中间元素的前面,我们可以把第二个指针指向该中间元素,搜索范围又缩小了。按上述思路,第一个指针总是指向前面的递增子数组中的元素,第二个指针总是指向后面的递增子数组中的元素,最终他们会指向两个相邻的元素,第二个指针指向的元素就是最小的元素。特例为当旋转的元素数为0时,我们应该能返回第一个指针指向的值(最小值)。还有当我们的输入为{1,0,1,1,1}或{1,1,1,0,1}时,此时我们无法通过判断中间值与两端值的大小关系来判断最小的数是在左边还是右边,此时应该用顺序查找法来找最小元素:

#include <iostream>
using namespace std;

int MinInOrder(int nums[], int index1, int index2) {    // 顺序查找法查找最小元素
    int res = nums[index1];
    for (int i = index1; i <= index2; ++i) {
        if (nums[i] < res) {
            res = nums[i];
        }
    }
    return res;
}

int Min(int nums[], int length) {
    if (nums == nullptr || length <= 0) {
        throw exception("Invalid input.");
    }

    int index1 = 0, index2 = length - 1, indexMid = index1;    // indexMid初始化为index1,当输入数组旋转了0个元素时,不会进入while循环,直接返回index1
    
    while (nums[index1] >= nums[index2]) {
        if (index2 - index1 == 1) {    // 两元素相邻时,返回后面的指针
            indexMid = index2;
            break;
        }

        indexMid = (index1 + index2) / 2;

        if (nums[index1] == nums[index2] && nums[index1] == nums[indexMid]) {
            return MinInOrder(nums, index1, index2);
        }

        if (nums[indexMid] >= nums[index1]) {    // 中间值大于等于左端值时,最小数在中间值右边
            index1 = indexMid;
        } else if (nums[indexMid] <= nums[index2]) {    // 中间值小于等于右端值时,最小数在中间值左边
            index2 = indexMid;
        }
    }

    return nums[indexMid];
}

int main() {
    int a[] = { 1,0,1,1,1 };
    cout << Min(a, sizeof(a) / sizeof(*a)) << endl;
    return 0;
}
posted @   epiphanyy  阅读(4)  评论(0编辑  收藏  举报  
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示