整数的快速排序算法

时间:2013-11-03 19:37:01

标签: c++ arrays algorithm sorting

我有未排序的整数对,它们代表一些时间间隔(第一个数字总是小于第二个数字)。问题是为每个时间间隔分配一个所谓的通道号(0..x)的整数,这样不碰撞的间隔将共享同一个通道。应使用尽可能少的频道。

例如,这些间隔将使用2个通道:

50 100 // 1

10 70 // 0

80 200 // 0

我使用计数排序实现它,按第一列对输入进行排序,然后使用线性搜索来查找彼此关联的链对。我首先将输入* const *数组复制到新数组,最后将值分配给输入数组中的正确位置。

是的,这是我从大学获得的一项任务,已经实施了,但任何人都可以告诉我如何让代码更快使用哪种算法,以便对的排序,链接将尽可能快?输入数组的长度最多为1000万个元素。

以下是代码:

#include <cstdlib>
#include <cstdio>
#include <iostream>
using namespace std;   

struct TPhone
 {
   unsigned int    m_TimeFrom;
   unsigned int    m_TimeTo;
   unsigned int    m_Channel;
 };

 class TElement
 {
 public:

  TPhone m_data;
  int index;

  TElement(TPhone  * const data, int index_)
  {
    m_data.m_TimeFrom=data->m_TimeFrom;
    m_data.m_TimeTo=data->m_TimeTo;
    m_data.m_Channel=-1;
    index=index_;
  }
  TElement()
  {
  }
 };

int FindNext(TElement** sorted_general_array, int general_array_size, int index_from)
{
  for (int i=index_from+1; i<general_array_size; i++ )
  {
    if (sorted_general_array[i]->m_data.m_TimeFrom > sorted_general_array[index_from]->m_data.m_TimeTo)
    {
      if (sorted_general_array[i]->m_data.m_Channel==(unsigned int)-1)
      {
        return i;
      }
    }
  }
  return -1;
}

int AssignChannels(TElement **sorted_general_array, int general_array_size)
{
  int current_channel=-1;
  for (int i=0; i<general_array_size; i++)
    {
      if (sorted_general_array[i]->m_data.m_Channel==(unsigned int)-1)
      {
        current_channel++;
        sorted_general_array[i]->m_data.m_Channel=current_channel;
        //cout << sorted_general_array[i]->m_data.m_TimeFrom << " " << sorted_general_array[i]->m_data.m_TimeTo << " " << sorted_general_array[i]->m_data.m_Channel << endl;
        int next_greater=i;
        while (1)
        {
          next_greater=FindNext(sorted_general_array,general_array_size,next_greater);
          if (next_greater!=-1)
          {
            sorted_general_array[next_greater]->m_data.m_Channel=current_channel;
            //cout << sorted_general_array[next_greater]->m_data.m_TimeFrom << " " << sorted_general_array[next_greater]->m_data.m_TimeTo << " " << sorted_general_array[next_greater]->m_data.m_Channel << endl;
          }
          else
          {
            break;
          } 
        }
      }
    }
    return current_channel;
}

int AllocChannels ( TPhone  * const * req, int reqNr )
 {
  //initialize
  int count_array_size=1700000;
  int * count_array;
  count_array=new int [count_array_size];
  for (int i=0; i<count_array_size; i++)
  {
     count_array[i]=0;
  }
  //
  int general_array_size=reqNr;
  TElement ** general_array;
  general_array=new TElement *[general_array_size];
  for (int i=0; i<general_array_size; i++)
  {
    general_array[i]= new TElement(req[i],i);
  }
  //--------------------------------------------------
  //counting sort
  //count number of each element
  for (int i=0; i<general_array_size; i++)
  {
    count_array[general_array[i]->m_data.m_TimeFrom]++;
  }
  //modify array to find postiions
  for (int i=0; i<count_array_size-1; i++)
  {
    count_array[i+1]=count_array[i+1]+count_array[i];
  }
  //make output array, and fill in the sorted data
  TElement ** sorted_general_array;
  sorted_general_array=new TElement *[general_array_size];

  for (int i=0; i <general_array_size; i++)
  {
    int insert_position=count_array[general_array[i]->m_data.m_TimeFrom]-1;
    sorted_general_array[insert_position]=new TElement;

    //cout << "inserting " << general_array[i]->m_data.m_TimeFrom << " to " << insert_position << endl;
    sorted_general_array[insert_position]->m_data.m_TimeFrom=general_array[i]->m_data.m_TimeFrom;
    sorted_general_array[insert_position]->m_data.m_TimeTo=general_array[i]->m_data.m_TimeTo;
    sorted_general_array[insert_position]->m_data.m_Channel=general_array[i]->m_data.m_Channel;
    sorted_general_array[insert_position]->index=general_array[i]->index;


    count_array[general_array[i]->m_data.m_TimeFrom]--;
    delete  general_array[i];
  }
  //free memory which is no longer needed
  delete [] general_array;
  delete [] count_array;
  //--------------------------------------------------

  int channels_number=AssignChannels(sorted_general_array,general_array_size);
  if (channels_number==-1)
  {
    channels_number=0;
  }
  else
  {
    channels_number++;
  }

  //output
  for (int i=0; i<general_array_size; i++)
  {
    req[sorted_general_array[i]->index]->m_Channel=sorted_general_array[i]->m_data.m_Channel;
  }


  //free memory and return
  for (int i=0; i<general_array_size; i++)
  {
    delete sorted_general_array[i];
  }
  delete [] sorted_general_array;

  return channels_number;
 }                                                             


int main ( int argc, char * argv [] )
 {
   TPhone ** ptr;
   int cnt, chnl;

   if ( ! (cin >> cnt) ) return 1;

   ptr = new TPhone * [ cnt ];
   for ( int i = 0; i < cnt; i ++ )
    {
      TPhone * n = new TPhone;
      if ( ! (cin >> n -> m_TimeFrom >> n -> m_TimeTo) ) return 1;
      ptr[i] = n;
    }

   chnl = AllocChannels ( ptr, cnt );

   cout << chnl << endl;
   for ( int i = 0; i < cnt; i ++ )
    {
      cout << ptr[i] -> m_Channel << endl;
      delete ptr[i];
    }
   delete [] ptr; 
   return 0;
  }

6 个答案:

答案 0 :(得分:4)

这个问题已经有了一个公认的答案。但是我想描述一种与接受的答案略有不同的方法。

您需要衡量

如果不进行测量,您无法告诉任何有关性能的信息。并且衡量我们需要测试用例。所以在我看来,第一项工作是创建一个可以生成测试用例的程序。

我做了一大堆假设,这可能是不正确的,并生成以下代码来生成测试用例:

#include <iostream>
#include <random>

int
main()
{
    const unsigned N = 10000000;
    std::mt19937_64 eng(0);
    std::uniform_int_distribution<unsigned> start_time(0, N);
    std::chi_squared_distribution<> duration(4);
    std::cout << N << '\n';
    for (unsigned i = 0; i < N;)
    {
        unsigned st = start_time(eng);
        unsigned et = st + static_cast<unsigned>(duration(eng));
        if (et > st)
        {
            std::cout << st << ' ' << et << '\n';
            ++i;
        }
    }
}

可以改变N的值,随机数引擎上的播种范围(如果不是随机数引擎的选择),开始时间的范围以及概率的类型/形状持续时间的分布。我凭空掏出了这些选择。您的教授可能对此问题的合理测试用例的生成有更好的想法。但是测量某事比测量什么更好。

使用std::lib

标准库中充满了容器和算法。这个代码不仅被调试,而且效率很高。重复使用此代码是一种很好的编码风格,因为:

  1. 它教你识别容器,何时使用什么容器。
  2. 它教你识别算法,以及何时使用什么算法。
  3. 它可以帮助您确定需要,并在std:lib未提供容器和算法时对其进行编码。
  4. 它使您的代码更容易为其他人阅读,因为他们将了解std定义的容器和算法。
  5. 它使您的代码更容易调试,因为代码中的错误概率远高于std::lib中错误的概率(尽管概率都不为零)。
  6. 例如

    我使用I / O扩充了您的TPhone结构,以减轻您在main中执行的I / O的复杂性:

    friend
    std::istream&
    operator>>(std::istream& is, TPhone& p)
    {
        return is >> p.m_TimeFrom >> p.m_TimeTo;
    }
    
    friend
    std::ostream&
    operator<<(std::ostream& os, const TPhone& p)
    {
        return os << '{' <<  p.m_TimeFrom << ", "
                         <<  p.m_TimeTo << ", "
                         << p.m_Channel << '}';
    }
    

    我选择vector<TPhone>来接听所有电话。这简化了这一点:

    int main ( int argc, char * argv [] )
     {
       TPhone ** ptr;
       int cnt, chnl;
    
       if ( ! (cin >> cnt) ) return 1;
    
       ptr = new TPhone * [ cnt ];
       for ( int i = 0; i < cnt; i ++ )
        {
          TPhone * n = new TPhone;
          if ( ! (cin >> n -> m_TimeFrom >> n -> m_TimeTo) ) return 1;
          ptr[i] = n;
        }
    

    到此为止:

    int main()
    {
        using namespace std;
        vector<TPhone> ptr;
        int cnt;
        if (!(cin >> cnt)) return 1;
        ptr.reserve(cnt);
        for (int i = 0; i < cnt; ++i)
        {
            TPhone n;
            if (!(cin >> n)) return 1;
            ptr.push_back(n);
        }
    

    事实证明我的版本比你的版本更有效率。我只是通过学习如何使用std::vector来获得这种效率&#34;免费#34;

    AllocChannels现在可以使用std::vector<TPhone>&

    int
    AllocChannels(std::vector<TPhone>& ptr)
    

    在这里,我使用了我能想到的最简单的算法。不是因为我认为它可能是最快的,而是因为你需要一个基线来衡量。事实证明,简单并不总是很慢......

    int
    AllocChannels(std::vector<TPhone>& ptr)
    {
        using namespace std;
        if (ptr.size() == 0)
            return 0;
        // sort TPhone's by x.m_TimeFrom
        sort(ptr.begin(), ptr.end(), [](const TPhone& x, const TPhone& y)
                                           {
                                               return x.m_TimeFrom < y.m_TimeFrom;
                                           });
       // Create channel 0 and mark it as busy by the ptr[0] until ptr[0].m_TimeTo
        vector<unsigned> channels(1, ptr.front().m_TimeTo);
        ptr.front().m_Channel = 0;
       // For each call after the first one ...
        for (auto i = next(ptr.begin()); i != ptr.end(); ++i)
        {
            // Find the first channel that isn't busy at this m_TimeFrom
            auto j = find_if(channels.begin(), channels.end(),
                                               [&](unsigned tf)
                                                 {
                                                     return tf < i->m_TimeFrom;
                                                 });
            if (j != channels.end())
            {
               // Found a non-busy channel, record it in use for this call
               i->m_Channel = j - channels.begin();
               // Mark the channel busy until m_TimeTo
               *j = i->m_TimeTo;
            }
            else
            {
                // Record a new channel for this call
                i->m_Channel = channels.size();
                // Create a new channel and mark it busy until m_TimeTo
                channels.push_back(i->m_TimeTo);
            }
        }
        return channels.size();
    }
    

    我使用了一些C ++ 11功能,因为它们很方便(例如auto和lambdas)。如果您没有这些功能,那么它们很容易在C ++ 03中解决。我使用的基本算法是按m_TimeFrom排序,然后在排序的调用列表中进行线性遍历,并且对于每个调用,通过一组通道进行线性搜索,寻找一个不使用(如果所有都在使用中,则创建一个新的)。请注意使用标准算法sortfind_if。没有意义重新实现这些,特别是对于基线测试用例。

    我使用<chrono>来计算所有内容:

    auto t0 = chrono::high_resolution_clock::now();
    int chnl = AllocChannels(ptr);
    auto t1 = std::chrono::high_resolution_clock::now();
    

    我以完全相同的方式检测您的代码,以便我可以测试它们。以下是我的结果,首先生成一个长度为= {100,1000,10000,100000,1000000,10000000}的测试用例,并且对于每个长度,首先运行您的代码,然后使用此输出:

    cout << "#intervals = " << cnt << '\n';
    cout << "#channels = " << chnl << '\n';
    cout << "time = " << chrono::duration<double>(t1-t0).count() << "s\n";
    

    这是我得到的:

    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out > test.dat
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 100
    #channels = 10
    time = 0.00565518s
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 100
    #channels = 10
    time = 6.934e-06s
    
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out > test.dat
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 1000
    #channels = 17
    time = 0.00578557s
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 1000
    #channels = 17
    time = 5.4779e-05s
    
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out > test.dat
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 10000
    #channels = 16
    time = 0.00801314s
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 10000
    #channels = 16
    time = 0.000656864s
    
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out > test.dat
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 100000
    #channels = 18
    time = 0.0418109s
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 100000
    #channels = 18
    time = 0.00788054s
    
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out > test.dat
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 1000000
    #channels = 19
    time = 0.688571s
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 1000000
    #channels = 19
    time = 0.093764s
    
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out > test.dat
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    Segmentation fault: 11
    $ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
    $ a.out < test.dat
    #intervals = 10000000
    #channels = 21
    time = 1.07429s
    

    <强>摘要

    这里没有人,包括我自己,预测最简单的解决方案会以惊人的数量始终击败你的第一次尝试。这可能是我生成的测试用例的一个特征。通过生成其他测试用例进行测量,这将是您学习的其他内容。

    我不知道N = 10000000的案例的分段错误的原因。我没有花时间研究你的代码。坦率地说,我觉得你的代码很复杂。

    我忽略了写一个正确性测试。那应该是我的第一步。输出是否正确?我懒得,只是瞥了一下N == 100的案子,看看它是否正确。

    由于重复使用了std :: containers和算法,我的代码实际上比你的代码更容易调整性能。例如,您可以尝试std::lower_bound(二分搜索)代替std::find_if衡量,如果它有所改善(我不打赌,但是你)应该衡量,并使用你尊重的test.dat

    将代码分解为容器和算法。重复使用std定义的容器和算法,否则创建自己的容器和算法,可以在将来的编码中重复使用。作为一名学生,我希望std定义的那些适用于大多数用例。

    始终测试正确性(因为我在这里没有这样做:-))不要在没有测量的情况下假设任何关于性能的事情。二进制搜索并不总是比线性搜索更快,即使它具有更好的渐近复杂度。输入数据会强烈影响算法的性能。了解如何生成各种输入数据,以了解您的算法如何受到影响。 <random>非常适合这项任务。

答案 1 :(得分:2)

将条目存储在std::vector<TPhone>而不是TPhone **中。这将在内存中连续布局连续的TPhone个对象,从而减少缓存未命中。

unsigned int的成员试用TPhone以外的其他数据类型。有关您可以尝试的类型,请参阅<cstdint>

答案 2 :(得分:0)

设[a i ,b i )为你的间隔,i = 1,...,n。您想设计一个函数通道(i),它返回每个n个区间的通道号。

您唯一的限制是没有两个相交的间隔可以在同一个通道上。这对应于一个无向图,其中您的间隔是顶点,当且仅当相应的间隔相交时,两个顶点之间有一条边。

如果这些顶点形成independent set,您可以将通道C分配给特定的一组顶点(间隔)。

您希望找到一组此表单的独立集合,其中所有表单的并集覆盖图形,并且它们是成对不相交的。你想要尽可能少的独立集。

找到最大独立集的(相关)问题是NP完全的。所以我认为你不应该期望找到一个多项式时间算法来找到给你最小通道数的解决方案。

更现实的期望有以下两种形式之一:(A)花费超多项式时间来解决问题,或者(B)使用可能无法为您提供全局最优的近似算法。

对于(B)你可以这样做:

feasible(M)

    initialize M empty channels (lists of intervals)

    sort intervals by a_i value

    for each interval I = [a_i, b_i):

        insert it into the channel for which the most recent
        interval is closest to the current interval (but not intersecting)

        if I cannot be inserted at the end of any channel, return false

    return true //the M channels are a feasible solution

现在使用此程序,您可以指数搜索可行的最小M,返回true。

尝试M = 1,2,4,8,16 ......直到你遇到第一个M = 2 k ,使得feasible(M)返回true。然后在2 k - 1 和2 k 之间进行二元搜索,找到最小值M.

答案 3 :(得分:0)

如果您对集合进行了排序,为什么要使用线性搜索?使用二进制搜索。

答案 4 :(得分:0)

如果您希望算法快速,则应尽可能减少搜索。此外,您不需要知道哪些区间被“链接在一起”以确定每个区间的正确信道(即,不使用比绝对必要的更多信道)。以下是我将用于获得最佳性能的步骤/技术:

  1. 像这样定义你的间隔类,添加两个内联函数定义(我使用TimeDescriptor的结构只是风格问题,而不是这段代码完全是时尚的):

    typedef struct TimeDescriptor {
        unsigned time;
        bool isEnd;
    } TimeDescriptor;
    
    class TimeInterval {
        public:
            TimeDescriptor start, end;
            unsigned channel;
    
            TimeInterval(unsigned startTime, unsigned endTime) {
                start = (TimeDescriptor){ startTime, false };
                end = (TimeDescriptor){ endTime, true };
            }
    }
    
    inline TimeInterval* getInterval(TimeDescriptor* me) {
        return (me->isEnd) ? (TimeInterval*)(me - 1) : (TimeInterval*)me;
    }
    
    inline TimeDescriptor* getOther(TimeDescriptor* me) {
        return (me->isEnd) ? (me - 1) : (me + 1);
    }
    
  2. 创建一个指向所有TimeDescriptors的指针数组,每个TimeInterval两个指针(一个用于开始,另一个用于结束)。

  3. 按时间排序此TimeDescriptor指针数组。确保使用isEnd标志作为辅助排序键。我不确定如何定义区间碰撞,i。即两个间隔(20,30)和(30,40)是否发生冲突,如果它们发生冲突,在开始时间之后用相同的值对结束时间进行排序,如果它们没有冲突,则在开始时间之前用相同的值对结束时间进行排序

    无论如何,我建议只使用标准的快速排序实现来对阵列进行排序。

  4. 为未使用的频道号创建堆栈。关于这个堆栈的重要事项是:它必须允许你在恒定时间内获取/推送一个通道号,理想情况是通过在内存中更新不超过两个数字;它必须是无底的,我。即它必须允许你弹出任意数量的值,产生一个递增的整数序列。

    实现这种堆栈的最简单方法可能是编写一个使用std::vector<unsigned>存储空闲通道的小类,并跟踪所用的最大通道数。无论何时无法从内部存储器处理弹出请求,都会通过将最大通道编号递增1来生成新的通道编号。

  5. 遍历已排序的TimeDescriptors数组。每次遇到开始时间时,都会获取一个通道编号,并将其存储在相应的TimeInterval中(使用getInterval())。每次遇到结束时间时,请将其频道编号推回到空闲频道阵列。

  6. 当您通过时,您的免费频道堆栈将告诉您同时使用的最大频道数,并且每个TimeInterval将包含要使用的正确频道号。您甚至可以通过简单地通过通道号来使用TimeInterval数组来有效地计算共享通道的所有区间链...

答案 5 :(得分:0)

很抱歉在这里成为一名死灵法师,但在阅读了问题并发布了答案之后,我真的不能放过这个。

频道指配算法:

这个问题有一个非常有效的贪婪解决方案。考虑以下递归:

  • 将第一个间隔分配给通道1
  • 对于每个剩余的间隔:
    • 对于每个频道(分配至少1个间隔):
      • 如果指定的间隔不存在冲突,请为此通道指定间隔并处理下一个间隔。
    • 如果所有频道都存在冲突,请将时间间隔分配给新频道。

这将产生最佳通道数。通过间隔数量的归纳,证明是微不足道的。

对输入进行排序

速度的关键在于如果没有冲突&#34;。这意味着将对已处理的内容和剩余的内容进行比较,并且应该很容易说服自己,首先对输入进行排序比在处理过程中对它们进行排序要快得多(或不对它们进行排序)。

如果您不相信,请考虑以下两个极端:

  1. 所有区间重叠。
  2. 没有间隔重叠。
  3. 选择排序算法

    我们需要按开始,然后结束时间对输入进行排序。如果我们选择稳定的排序,并且首先按结束时间排序,然后按开始时间排序,这很容易。如果所有值都是整数,则Counting Sort的稳定版本可能是最佳选择;输入数量远远大于输入范围;和内存使用不是一个重要的考虑因素。在这些条件下,这种排序是线性的。

    对频道进行排序

    通过对输入进行排序,我们只需将每个间隔与分配给每个通道的最后一个间隔进行比较。在没有间隔重叠的极端情况下,该算法是线性的:O(n)排序,+ O(n)处理= O(n)。在所有区间重叠的极端的另一端,没有进一步改进,算法将是二次的。

    为了改善这一点,而不是与所有频道进行比较,如果频道按最早的结束时间排序,那么与第一个频道的冲突将自动指示所有频道发生冲突。然后,对于每个区间,我们只需要进行1次比较,以及维持通道的排序顺序所需的一切。

    为此,我建议将频道保持在最小堆中(按结束时间)。比较所需的通道始终位于顶部。看看那个频道,然后:

    • 如果存在重叠,请创建一个新通道,将其添加到堆中。这个新频道可能需要以O(lg m)为代价向上移动,其中m是当前的频道数。
    • 否则,弹出最小通道O(lg m),向其添加间隔(更改它的值)并将其重新添加回堆中通常为O(lg m)。

    在最糟糕的噩梦场景中,排序的间隔将具有单调增加的开始时间,并且单调减少结束时间。这给了我们算法的最坏情况O(n + lg 1 + lg 2 + ... + lg n)= O(n + lg(n!))= O(n + n lg n)= O (n lg n)

    真实世界

    渐近更好并不总是更好。它实际上取决于输入的分布以及输入的大小。我确信这里描述的算法优于其他算法,但是在渐进相同的选择的实现中肯定存在空间,但会产生不同的结果。