基数排序,非负数 快排

本文深入解析基数排序算法,包括其原理、实现步骤及代码示例。通过实例展示基数排序如何利用链表和队列进行多位数排序,适用于大量数据的快速处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

#include <iostream>
#include <tuple>
#include <list>
using namespace std;

// 基数排序 简单 速度快 多一倍存储空间
// 基数: 如 10进制的基数为10 8进制的基数为8 对26个字母排序基数就是26 等等
//      最低位优先, 基数 == 链表的个数
// 原理:
//  每次按照数据每个位的数依次排序, 个位 十位 百位...
//  将中间结果存放在对应的10个容器中 (queue, FIFO)

void print(const list<int>& l)
{
    auto it = l.begin();
    while(it != l.end()) 
    { 
        cout << *it << " "; 
        it++; 
    }
     cout << endl;
}

template<typename T>
tuple<int, int> digits(const list<T>& l)
{
    auto it = l.begin();
    int ds = 10;
    int n = 1, num = 0;
    while(it != l.end()) // 遍历链表的所有数据
    {
        while(*it > ds)  // 找寻最大位数
        {
            ds *= 10;
            n++;
        }
        it++;
        num++;
    }
    return make_tuple(num, n);
}
template<typename T>
void radixSort(list<T>& l)
{
    // 得到该链表 数据总数 和 最大数的位数
    auto t = digits(l);
    cout << "\nnums: " << get<0>(t) << "  max digits: " << get<1>(t) <<  endl;

    list<T> lbuf[10]; // 十进制数,需要10个链表 存放个.十.百.千.位 比较的大小结果
    int ds = 1;       // 取出对应位的数字, 比较每一位时, 都对ds取整 ds为1 10 100... 然后对10取余
    for( ; get<1>(t)>0; get<1>(t)--) // n位 排序n次
    {
        // 对应某位的数,存放在缓存中
        auto it = l.begin(); // 比较完某一位后l被改变,需要重新指向
        for( ; it != l.end(); it++)  // nums个数据
        {
            lbuf[*it / ds % 10].push_back(*it); // 按照 对应位数字 把 该数据放入 对应缓存链表
        }

        // 从缓存中取出按某位排序后的数据, 放回原链表
        int n = 0; // 缓存链表索引
        l.clear(); // 清空原链表
        while(n < 10) // 从缓存链表取出数据 刷新原链表
        {
            while(!lbuf[n].empty()) // 清空某个缓存链表
            {
                l.push_back(lbuf[n].front()); // 取出数据 保存到 l
                lbuf[n].pop_front();          // pop掉该缓存
            }
            n++; // 缓存链表索引
        }
        ds *= 10;// 取整自乘, 1, 10, 100, 1000...
    }
}


int main()
{
    list<int> l{54, 23, 1002, 999, 2, 19, 51, 41, 22, 4}; // 10个数
    print(l);
    
    radixSort(l);
    print(l);

    return 0;
}

快排 清晰明了https://blog.youkuaiyun.com/morewindows/article/details/6684558

<think>嗯,用户想了解基数排序中如何处理负数的问题。我之前学过基数排序,不过主要针对正整数的情况。负数的话,可能需要调整一下算法。让我先回忆一下基数排序的基本原理。基数排序是按照每个位上的数字来分配桶,从低位到高位依次进行序,这样最终能得到有序的数组。对于正整数来说,这个过程很直观,但遇到负数,就需要处理符号的问题了。 用户提供的几个引用中提到,负数处理有不同的方法。比如引用1提到有两种方式,但没有具体说明。引用2和3则提到了一些具体的解决方案,比如调整数位计算公式或者分离正负数。引用4则介绍了基数排序的基本概念,但没有详细说明负数处理。 首先,负数在计算机中以补码形式存在,最高位是符号位。如果直接按照绝对值序,负数会比正数大,但实际负数应该在正数前面。所以可能需要将负数转换为正数来处理,或者在分配桶的时候考虑符号。 引用2提到在计算数位时使用公式,比如将数位计算调整为 (num / exp) % 10 + 10,这样负数得到的余数加上10后变为正数,可以直接分配到0-9的桶中,不需要额外空间。这种方法可能有效,但需要验证是否正确。 引用3提到在处理负数时,可以将负数转换为正数,然后序后再调整回来。例如,将所有数加上一个偏移量,使其变为负数序后再减去偏移量。这种方法可能适用于所有数都能被偏移的情况,比如当知道最小负数时,可以统一加上绝对值最大的负数,使所有数变为负。 另外,引用4提到基数排序是稳定序,所以可能需要保持稳定性。处理负数时,可能需要将正负分开处理。例如,先处理负数,取绝对值按高位序,逆序列,然后再处理正数。这样整体数组就会负数在前,正数在后,且各自有序。 不过,如何具体实现呢?比如,在Java中,可能需要将数组分成正负两部分,分别处理。或者,在每次按位序时,调整负数位的计算方式,使其不影响整体的序顺序。 比如,对于负数来说,其高位可能更大,但实际数值更小。所以,在按位序时,可能需要反转负数的位值,或者在分配桶时调整顺序。例如,对于负数,可以将其各位取补,然后按照正数的方式序,最后再反转顺序。 或者,采用一种更简单的方法,将所有数加上一个偏移量,使得最小值变为0或正数,然后进行基数排序,最后再减去偏移量。例如,如果数组中最小的数是-100,那么每个数都加上100,这样所有数都负,序后再减去100即可恢复原值。这种方法可能简单有效,但需要知道数组中的最小数,或者先遍历数组找到最小值。 不过,这样会增加时间复杂度吗?基数排序的时间复杂度是O(nk),其中k是最大位数,加上一次遍历找最小值,应该是可以接受的。 另外,引用2中提到的方法不需要额外处理负数,而是通过调整数位计算公式,使得负数也能正确分配到桶中。例如,当计算某一位的数字时,使用(num / exp) % 10 + 10,这样负数得到的余数会是负数,加上10后变为正数,分配到对应的桶里。例如,假设当前处理的是个位,num是-123,那么( -123 / 1 ) %10 = -3,加上10后得到7,因此个位是7,分配到第7个桶。这样处理的话,负数会被正确分配到0-9的桶中,而不需要额外空间。但这种方法是否会影响序的正确性呢? 比如,假设有两个负数,-123和-124,处理个位时,-123的个位是7,-124的个位是6,这样序后个位较大的数会被在后面,但实际数值上-123比-124大,所以正确的顺序应该是在数组中更靠前。所以,这样的处理可能需要考虑整个数的符号,或者在更高位进行调整。 可能这种方法需要将符号位也作为一个位数来处理。例如,在最高位处理时,符号位作为单独的位来处理。对于32位整数,最高位(第31位)是符号位,负数的话该位是1,正数是0。所以,在最高位处理时,可以按符号位来序,这样所有负数在正数前面。然后,在剩下的位中,负数需要按补码的形式来处理,或者进行某种转换。 不过,这样的处理可能比较复杂,尤其是当处理不同位的时候。因此,可能更简单的方法是使用偏移量,将所有数转换为负数,进行基数排序,再转换回来。 例如,步骤如下: 1. 遍历数组,找到最小值min(负数)。 2. 所有元素加上偏移量offset = -min,使得所有数变为负数。 3. 对转换后的数组进行基数排序。 4. 序后的数组每个元素减去offset,恢复原值。 这种方法简单有效,但需要额外的空间来存储转换后的数组吗?其实,可以在原数组上直接操作,但需要处理可能的溢出问题,尤其是当数组元素很大时,加上offset可能导致整型溢出。不过,在实际情况中,可能需要确保数值范围不会溢出。 另一种方法是分离正负数,分别对负数取绝对值进行序,然后反转顺序,再和正数合并。例如: 1. 将数组分为负数负数两部分。 2. 对负数部分,取绝对值进行基数排序,然后反转顺序。 3. 对负数部分进行基数排序。 4. 合并两部分,负数在前,正数在后。 这种方法避免了偏移量的计算,但需要处理两次序,增加了时间复杂度吗?基数排序的时间复杂度是线性的,所以两次序的时间复杂度仍然是O(nk),不会有太大的问题。不过,分离数组需要额外的空间,或者可以在原数组上进行操作。 比如,在Java中,可以使用List来分离正负数,或者直接在原数组中操作。这种方法的关键在于正确负数部分。例如,对负数取绝对值后序,得到的是从大到小的顺序,所以需要反转后才是从小到大的顺序。例如,原负数数组是-5, -3, -1,取绝对值后序得到1,3,5,反转后为5,3,1,对应的原数是-1, -3, -5,但实际正确的顺序应该是-5, -3, -1,所以可能需要先按绝对值序,然后逆序列,再转换为负数。 或者,对负数部分进行基数排序时,按绝对值序,但序后的顺序是从小到大,比如绝对值的序是1,3,5,对应的原数是-1, -3, -5,但这与原数的正确顺序相反。因此,正确的方法是,对负数部分进行基数排序时,按绝对值从大到小序,这样原数就会从小到大列。或者,在序完成后,将负数部分逆序列。 例如,假设负数部分是-5, -3, -1,他们的绝对值是5,3,1。如果按基数排序从小到大列绝对值,得到1,3,5,对应的原数是-1, -3, -5,这显然顺序不对。因此,需要对负数部分进行基数排序时,按照绝对值从大到小的顺序列。或者,在基数排序时,将比较的顺序反转。 这可能比较复杂。另一种方法是,对负数部分进行基数排序时,每一位的处理需要考虑符号的影响。例如,在最高位处理符号位,然后对数值部分进行逆序处理。这可能需要修改基数排序的位处理逻辑。 回到引用中的方法,引用3提到在处理负数时,可以将其视为正数加上偏移量,例如所有数都减去最小值,使得最小值为0,然后进行基数排序,再恢复。这可能是一个通用的解决方案,不需要分离正负数,且处理简单。 例如,步骤: 1. 找出数组中的最小值min。 2. 将所有元素加上offset = -min,使得所有元素变为负数。 3. 对转换后的数组进行基数排序。 4. 将序后的数组每个元素减去offset,恢复原值。 这种方法不需要处理正负数分离,统一转换后处理。但需要注意,当原数组中存在负数时,加上offset可能导致数值变得很大,增加基数排序的位数,从而影响效率。例如,如果原数组中有一个很大的负数,那么转换后的数值可能会常大,导致需要处理更多的位数。 因此,这种方法在数据范围较大时可能效率较低,但在实际应用中,如果数值范围可控,这可能是一个简单有效的方法。 总结来说,处理基数排序中的负数问题,常见的方法有: 1. 偏移量法:将所有数转换为负数序后再恢复。 2. 分离正负法:分别处理正负数组,负数按绝对值逆序列。 3. 调整数位计算:修改数位计算公式,使负数能正确分配到桶中,无需额外处理。 用户提供的引用中提到了这些方法的变种。例如,引用2中的数位调整法,可能属于第三种方法。引用3中的偏移量法属于第一种。而分离正负法则可能需要结合引用中的其他思路。 现在,我需要将这些方法整理成一个清晰的回答,说明每种方法的步骤和优缺点,并给出示例代码或步骤。例如,在Java中,如何实现这些方法? 例如,偏移量法的Java实现步骤: - 找到数组中的最小值min。 - 计算offset = -min。 - 创建新数组,每个元素加上offset。 - 对转换后的数组进行基数排序。 - 恢复原值,每个元素减去offset。 需要注意,Java的数组操作需要考虑数据类型,比如int数组的处理。另外,基数排序通常用于整数,所以处理负数时需要确保转换后的数仍然是整数。 分离正负法的步骤: - 将数组分为负数负数两部分。 - 对负数数组取绝对值,进行基数排序,然后逆序,再转换为负数。 - 对负数数组进行基数排序。 - 合并两部分数组,负数在前,负数在后。 这种方法需要处理两次基数排序,但可能更高效,如果负数负数部分的数据范围较小的话。 调整数位计算的方法,如引用2中提到的公式:digit = (num / exp) % 10 + 10。这样,无论num是正是负,digit都会落在0-19的范围内吗?例如,当num为负数时,(num / exp) % 10可能为负数,比如-3,加上10后变为7,这样digit的范围是0-19?然后分配桶的时候,需要20个桶(0-19)?或者,因为加上10后,digit的范围是0-19,所以需要20个桶?这可能增加了桶的数量,但避免了处理正负数的分离。 例如,在每次基数排序的迭代中,每个位数的计算使用上述公式,这样负数会被分配到不同的桶中,但不需要额外处理符号。这种方法可能需要更多的桶,但代码相对简单。 不过,这样处理是否正确呢?例如,假设两个负数,-123和-124,在个位处理时,-123的个位是7,-124的个位是6,因此在序时,7比6大,所以-123会在-124后面,但正确的顺序应该是-124 < -123,所以这样处理会导致负数部分顺序错误。这说明这种方法可能有问题,因为对于负数来说,高位较大的数值反而更小。 因此,调整数位计算的方法可能需要更多的处理,比如在最高位处理符号位,或者在处理负数时反转序顺序。 可能,正确的做法是在处理每一位时,对于负数,其实际位数需要以补码的形式处理,或者在处理符号位时单独处理。例如,最高位作为符号位,负数分配在0号桶,正数在1号桶,这样负数在前面。然后,在处理数值部分时,负数需要逆序列,因为数值越大,绝对值越小(比如-1 > -2)。 这可能比较复杂,但可以结合符号位和数值位的处理。例如,在基数排序的第一轮处理符号位,将负数分配到前面的桶,正数分配到后面的桶。然后在处理数值位时,对负数部分按逆序处理,因为他们的绝对值越大,实际值越小。 比如,步骤: 1. 处理符号位:将负数放在一起,正数放在一起。 2. 对负数部分,按绝对值进行基数排序,但每一位的处理顺序相反(例如,大的数字在前面)。 3. 对正数部分正常进行基数排序。 4. 合并负数和正数部分。 这种方法需要修改基数排序的逻辑,特别是对负数部分的处理。例如,在每次分配桶时,负数部分的桶顺序是降序列的,而正数部分是升序列的。 这可能比较复杂,但可以确保负数部分的正确顺序。 综上所述,处理负数的方法有多种,各有优缺点。用户需要根据具体情况选择合适的方法。偏移量法简单但可能增加数值范围;分离正负法需要两次序但逻辑清晰;调整数位计算法需要修改基数排序的位处理逻辑,可能复杂但节省空间。 在回答用户的问题时,需要列举这些方法,并说明其步骤,可能给出代码示例。同时,引用相关的参考资料,如用户提供的引用1-4中的方法。 比如,引用2中提到的方法可以详细说明:在计算数位时,使用digit = (num / exp) % 10 + 10,这样负数得到的数位会被调整到0-19的范围,然后使用20个桶进行序。这样,不需要分离正负数,但需要更多的桶。例如,在Java代码中,基数排序的实现可以修改数位计算部分,并在每次迭代中使用20个桶。 但需要注意,这种方法是否正确。比如,对于负数,数位的处理是否会导致序错误。例如,假设两个负数,-12和-13,在处理个位时,-12的个位是(-12 /1 )%10 = -2,加上10后是8;-13的个位是(-13/1)%10 = -3,加上10后是7。所以,在序时,-12的个位是8,比7大,因此在个位序后,-12会在-13后面。但实际上,-12比-13大,正确的顺序应该是-13(更小)在前,-12在后。所以,这种方法在个位序后,-12在后面是正确的。然后在处理十位时,-12的十位是(-12 /10)%10 = -1%10 = -1(可能有问题?需要确认Java中的取模运算结果)。Java中,-1 % 10 的结果是9,因为Java的取模运算返回值的符号与被除数一致,所以-1%10=-1?或者,是否不同语言处理不同? 例如,在Java中,-1 % 10的结果是-1,所以当计算十位时,digit = (-1) +10=9。因此,-12的十位是9,而-13的十位是(-13/10)= -1.3,取整为-1,然后%10得到-1,加上10得到9。所以,十位都是9,所以在十位序时,顺序不变。这样,经过十位和个位的序,最终顺序是-13, -12,这正确吗? 其实,基数排序是从低位到高位依次序,所以个位序后,-12在-13后面,然后在十位序时,十位相同,所以个位的顺序保持不变。所以最终顺序是-13, -12,而正确的升序应该是-13 < -12,所以顺序正确。 这说明这种方法是可行的。因此,使用调整数位计算的方法,即digit = (num / exp) % 10 +10,可以正确地将负数分配到桶中,并且保持序的正确性。这可能是因为在每次位处理时,虽然负数得到不同的digit值,但由于基数排序是稳定序,最终能正确列顺序。 因此,引用2中的方法是有效的,可以在不分离正负数的情况下处理负数,只需要调整数位的计算方式,并使用更多的桶(20个)。 因此,在回答用户的问题时,可以总结三种方法,并给出示例代码或步骤,引用相关的资料。</think>### 基数排序处理负数的方法 基数排序本身基于数值的各个数位分配桶,但负数由于符号的存在需要特殊处理。以下是三种常用方法及其实现逻辑: --- #### **方法1:偏移量法(转换为负数)** 1. **步骤**: - 找到数组中的最小值$min$(可能为负数)。 - 将数组中所有元素加上偏移量$offset = -min$,使其变为负数。 - 对转换后的数组进行基数排序。 - 序完成后,所有元素减去$offset$恢复原值。 2. **优点**:逻辑简单,无需修改基数排序核心逻辑。 3. **缺点**:若数值范围较大,可能增加序的位数,影响效率[^3]。 **示例代码(Java)**: ```java public static void radixSortWithOffset(int[] arr) { int min = Arrays.stream(arr).min().getAsInt(); int offset = (min < 0) ? -min : 0; for (int i = 0; i < arr.length; i++) { arr[i] += offset; } radixSort(arr); // 调用标准基数排序 for (int i = 0; i < arr.length; i++) { arr[i] -= offset; } } ``` --- #### **方法2:分离正负法(独立处理负数)** 1. **步骤**: - 将数组分为负数负数两部分。 - 对负数部分取绝对值进行基数排序序后逆序并恢复符号。 - 对负数部分直接进行基数排序。 - 合并两部分(负数在前,正数在后)。 2. **优点**:避免数值范围扩大,减少位数计算开销。 3. **缺点**:需多次遍历数组,增加代码复杂度[^4]。 **示例代码(Java逻辑)**: ```java public static void radixSortSeparate(int[] arr) { List<Integer> negatives = new ArrayList<>(); List<Integer> positives = new ArrayList<>(); for (int num : arr) { if (num < 0) negatives.add(-num); else positives.add(num); } // 对负数序并恢复符号 int[] negArr = negatives.stream().mapToInt(i -> i).toArray(); radixSort(negArr); reverseArray(negArr); // 逆序后变为从小到大 for (int i = 0; i < negArr.length; i++) negArr[i] *= -1; // 对正数序 int[] posArr = positives.stream().mapToInt(i -> i).toArray(); radixSort(posArr); // 合并结果 System.arraycopy(negArr, 0, arr, 0, negArr.length); System.arraycopy(posArr, 0, arr, negArr.length, posArr.length); } ``` --- #### **方法3:调整数位计算(统一处理正负数)** 1. **步骤**: - 修改数位计算公式为: $$ \text{digit} = \left(\frac{\text{num}}{\text{exp}}\right) \% 10 + 10 $$ - 使用20个桶(0-19),覆盖正负数所有可能的数位值。 - 从低位到高位依次序,最终得到正确结果。 2. **优点**:无需分离数据,直接兼容正负数。 3. **缺点**:桶数量翻倍,略微增加空间复杂度[^2]。 **示例代码(Java核心逻辑)**: ```java public static void radixSortWithNegative(int[] arr) { final int RADIX = 20; // 0-19对应-10到+9 int max = Arrays.stream(arr).max().getAsInt(); int min = Arrays.stream(arr).min().getAsInt(); int exp = 1; // 计算最大绝对值的位数 int maxDigits = Math.max(String.valueOf(max).length(), String.valueOf(-min).length()); for (int i = 0; i < maxDigits; i++) { List<Integer>[] buckets = new ArrayList[RADIX]; for (int num : arr) { int digit = (num / exp) % 10 + 10; // 调整数位范围 if (buckets[digit] == null) buckets[digit] = new ArrayList<>(); buckets[digit].add(num); } // 收集数据 int index = 0; for (int j = 0; j < RADIX; j++) { if (buckets[j] != null) { for (int num : buckets[j]) { arr[index++] = num; } } } exp *= 10; } } ``` --- ### **方法对比** | 方法 | 适用场景 | 时间复杂度 | 空间复杂度 | |--------------------|-------------------------|------------|------------| | 偏移量法 | 数值范围可控,允许转换 | $O(nk)$ | $O(n+k)$ | | 分离正负法 | 需减少桶数量 | $O(nk)$ | $O(n)$ | | 调整数位计算 | 兼容性要求高 | $O(nk)$ | $O(n+20)$ | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值