What is the fastest possible way to sort an array of 7 integers?
这是一个分析扑克赔率的程序的一部分,特别是德州扑克。我有一个程序我很满意,但它需要一些小的优化来完善。
我使用这种类型(当然还有其他类型):
1 2 | type T7Cards = array[0..6] of integer; |
在决定如何对数组进行排序时,此数组有两个方面可能很重要:
有了这些信息,对这个数组进行排序的最快方法是什么?我使用Delphi,所以Pascal代码是最好的,但我可以读取C和伪代码,虽然速度慢了一点:—)
目前我使用的是快速排序,但有趣的是,这几乎不比泡沫排序快!可能是因为项目数量少。排序几乎占方法总运行时间的50%。
编辑:
梅森·惠勒问为什么需要优化。一个原因是该方法将被调用2118760次。
基本的扑克信息:所有的玩家都会得到两张牌(口袋),然后五张牌被发到桌子上(第一张牌叫做扑克牌,第二张牌是转身牌,最后一张牌是河牌。每个玩家都会挑选五张最好的牌来组成自己的手)
如果我口袋里有两张牌,p1和p2,我将使用以下循环来生成所有可能的组合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | for C1 := 0 to 51-4 do if (C1<>P1) and (C1<>P2) then for C2 := C1+1 to 51-3 do if (C2<>P1) and (C2<>P2) then for C3 := C2+1 to 51-2 do if (C3<>P1) and (C3<>P2) then for C4 := C3+1 to 51-1 do if (C4<>P1) and (C4<>P2) then for C5 := C4+1 to 51 do if (C5<>P1) and (C5<>P2) then begin //This code will be executed 2 118 760 times inc(ComboCounter[GetComboFromCards([P1,P2,C1,C2,C3,C4,C5])]); end; |
号
在写这篇文章时,我注意到还有一件事:数组的最后五个元素总是被排序的,所以这只是将前两个元素放在数组中的正确位置的问题。这应该简化一点。
因此,新的问题是:当最后5个元素已经排序时,对7个整数数组进行排序的最快方法是什么?我相信这可以通过一对夫妇来解决。国际单项体育联合会和掉期交易:
对于非常小的集合,插入排序通常可以击败快速排序,因为它的开销非常低。
警告您的编辑,如果您已经主要按照排序顺序(最后5个元素已经排序),插入排序肯定是一种方式。在一组几乎排序的数据中,它每次都会击败Quicksort,即使对于大型数据集也是如此。(尤其是大套!这是插入排序的最佳情况场景和快速排序的最差情况。)
不知道您是如何实现这一点的,但是您可以做的是使用一个52数组而不是7数组,当您得到它时,只需将卡直接插入其插槽中,因为它永远不会重复,这样您就不必对数组进行排序。这可能会更快,这取决于它是如何使用的。
我对德克萨斯州的hold'em不太了解:p1和p2是什么西服很重要,还是只有他们是同一西服或者不是同一西服才重要?如果只有suit(p1)==suit(p2)重要,那么您可以将这两个案例分开,对于p1/p2,您只有13x12/2种不同的可能性,并且您可以轻松地为这两个案例预先计算一个表。
否则,我建议如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | (* C1 < C2 < P1 *) for C1:=0 to P1-2 do for C2:=C1+1 to P1-1 do Cards[0] = C1; Cards[1] = C2; Cards[2] = P1; (* generate C3...C7 *) (* C1 < P1 < C2 *) for C1:=0 to P1-1 do for C2:=P1+1 to 51 do Cards[0] = C1; Cards[1] = P1; Cards[2] = C2; (* generate C3...C7 *) (* P1 < C1 < C2 *) for C1:=P1+1 to 51 do for C2:=C1+1 to 51 do Cards[0] = P1; Cards[1] = C1; Cards[2] = C2; (* generate C3...C7 *) |
(这只是一张牌p1的演示,你必须将它扩展到p2,但我认为这很简单。尽管会有很多输入法…)这样,排序就不需要任何时间。生成的排列已经排序。
由于最后5个项目已经排序,因此可以编写代码来重新定位前2个项目。因为您使用的是Pascal,所以我编写并测试了一个排序算法,它可以在大约62毫秒内执行2118760次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | procedure SortT7Cards(var Cards: T7Cards); const CardsLength = Length(Cards); var I, J, V: Integer; V1, V2: Integer; begin // Last 5 items will always be sorted, so we want to place the first two into // the right location. V1 := Cards[0]; V2 := Cards[1]; if V2 < V1 then begin I := V1; V1 := V2; V2 := I; end; J := 0; I := 2; while I < CardsLength do begin V := Cards[I]; if V1 < V then begin Cards[J] := V1; Inc(J); Break; end; Cards[J] := V; Inc(J); Inc(I); end; while I < CardsLength do begin V := Cards[I]; if V2 < V then begin Cards[J] := V2; Break; end; Cards[J] := V; Inc(J); Inc(I); end; if J = (CardsLength - 2) then begin Cards[J] := V1; Cards[J + 1] := V2; end else if J = (CardsLength - 1) then begin Cards[J] := V2; end; end; |
号
7种元素只有5040种排列。您可以通过编程方式生成一个程序,该程序在最小数量的比较中查找由输入表示的程序。它将是一个由
棘手的部分是决定在特定的内部节点中比较哪两个元素。为此,必须考虑祖先节点从根节点到特定节点(例如
一旦你有了排列,用一组最小的交换对它进行排序是很简单的。
对于7个数字,与比较数量相关的最有效的算法是福特约翰逊的。事实上,维基百科引用了一篇在谷歌上很容易找到的论文,声称福特约翰逊最适合47个数字。不幸的是,对FordJohnson的引用并不那么容易找到,而且算法使用了一些复杂的数据结构。
它出现在唐纳德·克努斯的《计算机编程艺术》第三卷上,如果你能读到那本书的话。
这里有一篇文章描述了FJ和一个更节省内存的版本。
无论如何,由于该算法的内存开销,我怀疑对于整数来说值得一试,因为与分配内存和操作指针的成本相比,比较两个整数的成本是相当便宜的。
现在,您提到5张卡片已经排序,您只需要插入两张。您可以使用这样的插入排序最有效地做到这一点:
1 2 3 4 | Order the two cards so that P1 > P2 Insert P1 going from the high end to the low end (list) Insert P2 going from after P1 to the low end (array) Insert P2 going from the low end to the high end |
如何做到这一点取决于数据结构。使用一个数组,你将交换每个元素,所以将p1放在1st、p2和7th(从高到低排列),然后将p1向上交换,然后将p2向下交换。有了一个列表,您只需要适当地修复指针。
然而,再一次,由于代码的特殊性,如果您遵循Nikie的建议,并且只为p1和p2可能出现在列表中的每个变体适当地生成for循环,这确实是最好的。
例如,对p1和p2进行排序,使p1 号 然后调用一个传递po1、po2、p1、p2、c1、c2、c3、c4、c5的函数,并让这个函数返回基于po1和po2的所有可能排列(这是36个组合)。 就个人而言,我认为这是你能做到的最快的。您完全不必订购任何东西,因为数据是预先订购的。不管怎样,在一些比较中,您需要计算开始和结束,但是它们的成本是最小化的,因为它们中的大多数都在最外层的循环上,所以它们不会重复太多。它们甚至可以以更多的代码复制为代价进行更优化。
2
3
4
5
6
7
8
9
10
11
Loop Po2 from Po1 + 1 to 6
If (Po2 == 1) C1start := P2 + 1; C1end := 51 - 4
If (Po1 == 0 && Po2 == 2) C1start := P1+1; C1end := P2 - 1
If (Po1 == 0 && Po2 > 2) C1start := P1+1; C1end := 51 - 5
If (Po1 > 0) C1start := 0; C1end := 51 - 6
for C1 := C1start to C1end
// Repeat logic to compute C2start and C2end
// C2 can begin at C1+1, P1+1 or P2+1
// C2 can finish at P1-1, P2-1, 51 - 3, 51 - 4 or 51 -5
etc
这是最快的方法:因为5卡列表已经排序,所以对两张卡列表进行排序(比较和交换),然后合并这两个列表,即O(K*(5+2)。在这种情况下(k)通常为5:循环测试(1)、比较(2)、副本(3)、输入列表增量(4)和输出列表增量(5)。那是35+2.5。抛出循环初始化,总共得到41.5条语句。
您还可以展开循环,这样可以节省8条语句或执行,但使整个例程的时间延长4-5倍,这可能会影响指令缓存命中率。
给定p(0到2),c(0到5)并复制到h(0到6)C()已排序(升序):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | If P(0) > P(1) Then // Swap: T = P(0) P(0) = P(1) P(1) = T // 1stmt + (3stmt * 50%) = 2.5stmt End P(2), C(5) = 53 \\ Note these are end-of-list flags k = 0 \\ P() index J = 0 \\ H() index i = 0 \\ C() index // 4 stmt Do While (j) < 7 If P(k) < C(I) then H(j) = P(k) k = k+1 Else H(j) = C(i) j = j+1 End if j = j+1 // 5stmt * 7loops = 35stmt Loop |
请注意,如果必须对所有7张卡进行真正排序,这比其他"最快"的算法要快:使用位掩码(52)将所有7张卡映射和位设置为所有可能52张卡(位掩码)的范围,然后扫描位掩码以查找设置的7位。这最多需要60-120条语句(但仍然比任何其他排序方法都快)。
使用最小排序。同时搜索最小和最大元素,并将它们放入结果数组中。重复三次。(编辑:不,我不会尝试从理论上测量速度:uu))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | var cards,result: array[0..6] of integer; i,min,max: integer; begin n=0; while (n<3) do begin min:=-1; max:=52; for i from 0 to 6 do begin if cards[i]<min then min:=cards[i] else if cards[i]>max then max:=cards[i] end result[n]:=min; result[6-n]:=max; inc(n); end for i from 0 to 6 do if (cards[i]<52) and (cards[i]>=0) then begin result[3] := cards[i]; break; end { Result is sorted here! } end |
号
在伪代码中:
1 2 3 4 5 6 7 8 9 10 11 12 | int64 temp = 0; int index, bit_position; for index := 0 to 6 do temp |= 1 << cards[index]; for index := 0 to 6 do begin bit_position = find_first_set(temp); temp &= ~(1 << bit_position); cards[index] = bit_position; end; |
。
这是bucket排序的一个应用程序,通常比所建议的任何比较排序都要快。
注:第二部分也可以通过在线性时间内对位进行迭代来实现,但实际上可能不会更快:
1 2 3 4 5 6 7 8 9 | index = 0; for bit_position := 0 to 51 do begin if (temp & (1 << bit_position)) > 0 then begin cards[index] = bit_position; index++; end; end; |
对于7个元素,只有很少的选项。您可以轻松地编写一个生成方法的生成器来对7个元素的所有可能组合进行排序。类似于这种方法的3个元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | if a[1] < a[2] { if a[2] < a[3] { // nothing to do, a[1] < a[2] < a[3] } else { if a[1] < a[3] { // correct order should be a[1], a[3], a[2] swap a[2], a[3] } else { // correct order should be a[3], a[1], a[2] swap a[2], a[3] swap a[1], a[3] } } } else { // here we know that a[1] >= a[2] ... } |
当然,7个元素的方法会更大,但并不难生成。
假设您在它的末尾需要一组卡片。
将原始卡映射到64位整数(或任何大于等于52位的整数)中的位。
如果在初始映射期间对数组进行了排序,则不要更改它。
将整数分为半字节-每个都对应于值0x0到0xF。
使用半字节作为对应的已排序子数组的索引。您将需要13组16个子数组(或仅16个子数组并使用第二个间接寻址,或者执行位操作而不是查找答案;更快的速度将因平台而异)。
将非空子数组连接到最终数组中。
如果需要,可以使用大于半字节的数组;字节将提供7组256个数组,使非空数组更可能需要连接。
这假设分支很昂贵,缓存数组访问也很便宜。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #include <stdio.h> #include <stdbool.h> #include <stdint.h> // for general case of 7 from 52, rather than assuming last 5 sorted uint32_t card_masks[16][5] = { { 0, 0, 0, 0, 0 }, { 1, 0, 0, 0, 0 }, { 2, 0, 0, 0, 0 }, { 1, 2, 0, 0, 0 }, { 3, 0, 0, 0, 0 }, { 1, 3, 0, 0, 0 }, { 2, 3, 0, 0, 0 }, { 1, 2, 3, 0, 0 }, { 4, 0, 0, 0, 0 }, { 1, 4, 0, 0, 0 }, { 2, 4, 0, 0, 0 }, { 1, 2, 4, 0, 0 }, { 3, 4, 0, 0, 0 }, { 1, 3, 4, 0, 0 }, { 2, 3, 4, 0, 0 }, { 1, 2, 3, 4, 0 }, }; void sort7 ( uint32_t* cards) { uint64_t bitset = ( ( 1LL << cards[ 0 ] ) | ( 1LL << cards[ 1LL ] ) | ( 1LL << cards[ 2 ] ) | ( 1LL << cards[ 3 ] ) | ( 1LL << cards[ 4 ] ) | ( 1LL << cards[ 5 ] ) | ( 1LL << cards[ 6 ] ) ) >> 1; uint32_t* p = cards; uint32_t base = 0; do { uint32_t* card_mask = card_masks[ bitset & 0xf ]; // you might remove this test somehow, as well as unrolling the outer loop // having separate arrays for each nibble would save 7 additions and the increment of base while ( *card_mask ) *(p++) = base + *(card_mask++); bitset >>= 4; base += 4; } while ( bitset ); } void print_cards ( uint32_t* cards ) { printf ("[ %d %d %d %d %d %d %d ] ", cards[0], cards[1], cards[2], cards[3], cards[4], cards[5], cards[6] ); } int main ( void ) { uint32_t cards[7] = { 3, 9, 23, 17, 2, 42, 52 }; print_cards ( cards ); sort7 ( cards ); print_cards ( cards ); return 0; } |
。
下面的代码接近最优。在做树的时候列一个要遍历的列表可以做得更好,但是我现在没时间了。干杯!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | object Sort7 { def left(i: Int) = i * 4 def right(i: Int) = i * 4 + 1 def up(i: Int) = i * 4 + 2 def value(i: Int) = i * 4 + 3 val a = new Array[Int](7 * 4) def reset = { 0 until 7 foreach { i => { a(left(i)) = -1 a(right(i)) = -1 a(up(i)) = -1 a(value(i)) = scala.util.Random.nextInt(52) } } } def sortN(i : Int) { var index = 0 def getNext = if (a(value(i)) < a(value(index))) left(index) else right(index) var next = getNext while(a(next) != -1) { index = a(next) next = getNext } a(next) = i a(up(i)) = index } def sort = 1 until 7 foreach (sortN(_)) def print { traverse(0) def traverse(i: Int): Unit = { if (i != -1) { traverse(a(left(i))) println(a(value(i))) traverse(a(right(i))) } } } } |
看看这个:
http://en.wikipedia.org/wiki/sorting_算法
你需要选择一个最坏情况下成本稳定的…
另一种选择是始终保持数组的排序,因此添加一张卡将自动保持数组的排序,这样您就可以跳过排序…
JRL所指的是桶排序。因为您有一组有限的离散的可能值,所以您可以声明52个bucket,并且只需在o(1)时间内将每个元素放到bucket中。因此,桶排序是O(N)。如果不保证不同元素的数量有限,最快的理论排序是O(n log n),类似于合并排序这样的快速排序。这只是最佳和最坏情况的平衡。
但长答案短,用桶分类。
如果您喜欢上面提到的建议保留一个始终保持数组排序的52元素数组,那么您可以保留另一个包含7个元素的列表,这些元素引用52元素数组中的7个有效元素。这样我们甚至可以避免分析52元素数组。
我想为了使这个方法真正有效,我们需要一个支持操作的链接列表类型的结构:insertatposition()和deleteatpposition(),并且在这方面是有效的。
考虑到最后5个元素总是被排序:
1 2 3 4 5 6 7 8 9 | for i := 0 to 1 do begin j := i; x := array[j]; while (j+1 <= 6) and (array[j+1] < x) do begin array[j] := array[j+1]; inc(j); end; array[j] := X; end; |
使用排序网络,就像在这个C++代码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | template<class T> inline void sort7(T data) { #define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);} //DD = Define Data, create a local copy of the data to aid the optimizer. #define DD1(a) register auto data##a=*(data+a); #define DD2(a,b) register auto data##a=*(data+a);register auto data##b=*(data+b); //CB = Copy Back #define CB1(a) *(data+a)=data##a; #define CB2(a,b) *(data+a)=data##a;*(data+b)=data##b; DD2(1,2) SORT2(1,2) DD2(3,4) SORT2(3,4) DD2(5,6) SORT2(5,6) DD1(0) SORT2(0,2) SORT2(3,5) SORT2(4,6) SORT2(0,1) SORT2(4,5) SORT2(2,6) CB1(6) SORT2(0,4) SORT2(1,5) SORT2(0,3) CB1(0) SORT2(2,5) CB1(5) SORT2(1,3) CB1(1) SORT2(2,4) CB1(4) SORT2(2,3) CB2(2,3) #undef CB1 #undef CB2 #undef DD1 #undef DD2 #undef SORT2 } |
如果你想给它传递一个迭代器或一个指针,就使用上面的函数;如果你想一个接一个地传递七个参数,就使用下面的函数。顺便说一句,使用模板可以让编译器生成真正优化的代码,所以除非您需要C代码(或其他语言的代码),否则不要使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | template<class T> inline void sort7(T& e0, T& e1, T& e2, T& e3, T& e4, T& e5, T& e6) { #define SORT2(x,y) {if(data##x>data##y)std::swap(data##x,data##y);} #define DD1(a) register auto data##a=e##a; #define DD2(a,b) register auto data##a=e##a;register auto data##b=e##b; #define CB1(a) e##a=data##a; #define CB2(a,b) e##a=data##a;e##b=data##b; DD2(1,2) SORT2(1,2) DD2(3,4) SORT2(3,4) DD2(5,6) SORT2(5,6) DD1(0) SORT2(0,2) SORT2(3,5) SORT2(4,6) SORT2(0,1) SORT2(4,5) SORT2(2,6) CB1(6) SORT2(0,4) SORT2(1,5) SORT2(0,3) CB1(0) SORT2(2,5) CB1(5) SORT2(1,3) CB1(1) SORT2(2,4) CB1(4) SORT2(2,3) CB2(2,3) #undef CB1 #undef CB2 #undef DD1 #undef DD2 #undef SORT2 } |
。
泡泡派是你的朋友。其他种类的代码开销过大,不适合于少量的元素
干杯
这是您的基本O(N)类型。我不知道和其他人相比如何。它使用展开的循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | char card[7]; // the original table of 7 numbers in range 0..51 char table[52]; // workspace // clear the workspace memset(table, 0, sizeof(table)); // set the 7 bits corresponding to the 7 cards table[card[0]] = 1; table[card[1]] = 1; ... table[card[6]] = 1; // read the cards back out int j = 0; if (table[0]) card[j++] = 0; if (table[1]) card[j++] = 1; ... if (table[51]) card[j++] = 51; |
。
如果您正在寻找一个非常低的开销,最佳排序,您应该创建一个排序网络。您可以使用Bose Nelson算法为7整数网络生成代码。
在最坏的情况下,这将确保固定数量的比较和相等数量的交换。
生成的代码很难看,但它是最佳的。
您的数据在一个已排序的数组中,如果需要,我假设您交换新的两个,所以也进行了排序,所以a.如果你想把它放在适当的地方,那么使用插入排序的形式;B.如果你想把结果放在另一个数组中,通过复制进行合并。
对于较小的数字,二元杂碎是多余的,三元杂碎无论如何都是合适的:一张新卡主要是分成两张和三张,即。2+3或3+2,两张卡片分为单张和双张,例如2+1+2。
因此,放置较小的新卡的最省时的方法是与[1]相比较(即。跳过一个[0]),然后向左或向右搜索以找到它应该替换的卡,然后交换并向右移动(移动而不是冒泡),与较大的新卡比较,直到找到它的位置。在这之后,你将向前移动两次(插入两张牌)。持有新卡(和交换卡)的变量应该是寄存器。
查找方法会更快,但会占用更多内存。
答案中有很多循环。考虑到他的速度要求和数据集的小规模,我不会做任何循环。
我没有尝试过,但我怀疑最好的答案是完全展开的泡沫类型。它也可能从组装中获得相当多的优势。
不过,我想知道这是否是正确的方法。你打算如何分析一个7张牌的手?我想你最终还是要把它转换成其他的表示来进行分析。4x13数组不是更有用的表示形式吗?(无论如何,这会使排序问题变得毫无意义。)