【问题标题】:Quicksort is extremely slow快速排序非常慢
【发布时间】:2018-01-28 20:38:30
【问题描述】:

我有一个 2D 数组,它有 4 列和 1921980 行 string array = new string[1921980, 4]。该数组应按第四列中的值排序。所以我从冒泡排序开始,它非常慢,甚至是并行的。它还会产生高 CPU 利用率。我切换到快速排序(递归),事情变得更快。问题是我在排序时调用了 2 个方法:

  • Console.Write 显示进度
  • StringToInt32自定义方法,比Convert.ToInt32Int32.Parse

但这会在一段时间后产生 StackOverFlowException。所以我决定使用快速排序(迭代)。感觉就像冒泡排序。我猜我的代码有问题。

    static int StringToInt32(string s)
    {
        int tmp = 0;

        switch (s[0])
        {
            case '-':
                for (int counter = 1; counter < s.Length; counter++)
                {
                    tmp = tmp * 10 + (s[counter] - '0');
                }

                tmp *= -1;
                return tmp;
                break;
            default:
                for (int counter = 0; counter < s.Length; counter++)
                {
                    tmp = tmp * 10 + (s[counter] - '0');
                }
                return tmp;
                break;
        }
    }

    static void QuickSort(ref string[,] array, int length)
    {
        Stack<int> stack = new Stack<int>();
        stack.Push(length - 1);
        stack.Push(0);

        int percent = -1;

        int look = 0;

        while (stack.Count != 0)
        {
            /**
            * Pop array or sub array start and end index
            */
            int left = stack.Pop();
            int right = stack.Pop();
            int newPivotIndex = Partition(ref array, left, right);

            /**
            *  divide right i.e. second sub array from newPivotIndex+1 to right
            *  here newPivotIndex+1 is index where right elements are greater or = than pivot
            *  use stack for storing array index and in while loop pop that right sub array indexes.
            */
            if ((newPivotIndex + 1) < right)
            {
                stack.Push(right);
                stack.Push(newPivotIndex + 1);
            }
            /**
            *  divide sub array from left to mid or newPivotIndex-1
            *  here newPivotIndex-1 is index where left elements are lesser or = than pivot
            *  use stack for storing array index and in while loop pop that left sub array indexes.
            */
            if ((newPivotIndex - 1) > left)
            {
                stack.Push(newPivotIndex - 1);
                stack.Push(left);
            }

            look++;
            if (look * 100 / length != percent)
            {
                percent = look * 100 / length;
                Console.Write("\r{0}%", percent);
            }
        }

        Console.Write("\r100%");
    }

    static int GetMedianPivot(ref string[,] array, int left, int right)
    {
        int mid = ((left + right) / 2);
        /** middle number of array is less than left number of array 
         *  then Swap middle and left number
         */
        if (StringToInt32(array[mid, 3]) < StringToInt32(array[left, 3]))
        {
            Swap(ref array, left, mid);
        }
        /** rightmost number of array is less than left number of array 
         *  then Swap right and left number
         */
        if (StringToInt32(array[right, 3]) < StringToInt32(array[left, 3]))
        {
            Swap(ref array, left, right);
        }
        /**
        * now right number is less than mid then Swap number which 
        * shifts median of three numbers into mid position
        */
        if (StringToInt32(array[right, 3]) < StringToInt32(array[mid, 3]))
        {
            Swap(ref array, mid, right);
        }
        /**
        * Shift Median or pivot from mid to rightmost position
        * i.e. out of partitioning index
        */
        Swap(ref array, mid, right);
        /**return pivot number value which is right most now.**/
        return StringToInt32(array[right, 3]);
    }

    static int Partition(ref string[,] array, int left, int right)
    {
        int pivot = StringToInt32(array[right, 3]); ;

        /**
        * Initialize low = left i.e. start index of array or logical sub-array
        * Initialize high = right-1 i.e. end index of array or logical sub array;
        * becuase right is pivot element so start from right-1;
        */
        int low = left;
        int high = right - 1;
        do
        {
            while (StringToInt32(array[low, 3]) < pivot && low < right - 1)
            {
                low++;
            }

            while (StringToInt32(array[high, 3]) >= pivot && high > left)
            {
                high--;
            }

            /** 
            * Swap elements when any left element is greater than pivot or  
            * any right element is less than pivot.
            */
            if (low < high)
            {
                Swap(ref array, low, high);
                low++;
                high--;
            }
        } while (low < high);
        /**
        *  Swap right most pivot to its right position i.e. at low, then 
        *  left elements are lesser and right elements are greater than pivot.
        */
        if (StringToInt32(array[low, 3]) > pivot)
        {
            Swap(ref array, low, right);
        }

        return low;
    }

    static void Swap(ref string[,] array, int i, int j)
    {
        string[] buffer = new string[4];

        buffer[0] = array[i, 0];
        buffer[1] = array[i, 1];
        buffer[2] = array[i, 2];
        buffer[3] = array[i, 3];

        array[i, 0] = array[j, 0];
        array[i, 1] = array[j, 1];
        array[i, 2] = array[j, 2];
        array[i, 3] = array[j, 3];

        array[j, 0] = buffer[0];
        array[j, 1] = buffer[1];
        array[j, 2] = buffer[2];
        array[j, 3] = buffer[3];
    }

【问题讨论】:

  • 每次交换创建新数组,在每次比较中将字符串转换为 int 等等......当然它会很慢。如果自定义 int 转换真的比框架中提供的更快,我会感到非常惊讶,但我猜这是可能的。
  • 为什么不先将所有字符串转换为整数然后对其进行排序,而不是转换每个迭代/交换/枢轴/等? int[,] 会不会更合适?如果必须,请先转换为该格式,对其进行排序,然后使用 ToString 复制回字符串数组。每次你可以提前做一次时,你都会做很多额外的工作
  • 这里没有问题。你写了一个非常糟糕的快速排序实现,它非常慢,而且......这是一个故事而不是一个问题。你有什么具体的、可以回答的问题?

标签: c# arrays sorting


【解决方案1】:

为什么你到处都使用 refs? Array 已经是一个指针,你可以将它高效地用作参数,使用 ref 并不快。 Sami Kuhmonen 也有一些重要的观察。

我会使用类似的东西:

int ArrSize = 1921980;
long[] tmp = new long[ArrSize];
for (int i = 0; i < ArrSize; ++i)
{
    tmp[i] = (((long)StringToInt32(array[i, 3])) << 32) | (long)i;
}

Array.Sort(tmp);

string[,] array2 = new string[ArrSize, 4];
for (int i = 0; i < ArrSize; ++i)
{
    int index = (int)tmp[i];
    array2[i, 0] = array[index, 0];
    array2[i, 1] = array[index, 1];
    array2[i, 2] = array[index, 2];
    array2[i, 3] = array[index, 3];
}

负数有问题吗?将负 int 转换为 long 会在高 32 位中产生 1。但在我们的案例中,它们被移出。高 32 位最终等于数组中的原始数字。为了比较,(不是那样做的,但你可以imagine that is)你一点一点地比较,如果你发现差异,你就会产生结果。从中可以看出,只要高 32 位存在差异,低 32 位无关紧要。 (long)i 是安全的,因为 i 是数组中的索引并且它始终是正数。 (int)tmp[i] 只是丢弃高 32 位 a 不会以任何方式改变低位。结论是,这不是问题。

另一个问题可能是,这是否稳定,也就是说,对于被认为相等的项目,这是否会保留原始序列中的项目顺序?如果它们相等,则它们的高 32 位没有区别。低 32 位是原始数组中的索引,这意味着它们代表原始顺序。如果高 32 位相等,则按低 32 位进行比较,即按原始序列中的顺序进行比较。这可能有点令人惊讶,但即使Array.Sort 本身不是这样,这种排序方式也是稳定的。

【讨论】:

  • 这比我写的要好得多...如果项目是否为负,它是否有问题?...符号位为 1 重新定义了其余部分的含义...想知道它是否是一个问题...不确定
猜你喜欢
  • 2021-06-06
  • 2010-10-04
  • 2013-10-08
  • 2012-10-04
  • 2022-09-27
  • 2021-03-11
  • 2010-11-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多