【数据布局】快排之三路划分+文件归并排序

打印 上一主题 下一主题

主题 1019|帖子 1019|积分 3057

一.快排

1.快排性能分析

决定快排性能的关键点是每次单趟排序后,key对数组的分割,假如每次选key基本二分居中,那么快排的递归树就是颗均匀的满二叉树,性能最佳。但是实践中固然不大概每次都是二分居中,但是性能也还是可控的。假如出现每次选到最小值/最大值,划分为0个和N-1的子问题时,时间复杂度为O(N^2),数组序列有序时就会出现如许的问题,在《八大排序》已经用三数取中或者随机选key解决了这个问题,也就是说我们解决了绝大多数的问题,但是现在还是有一些场景没解决(数组中有大量重复数据时),类似以下数据。
  1. //数组中有多个跟key相等的值
  2. vector<int> a1 = { 6,1,7,6,6,6,4,9 };
  3. vector<int> a2 = { 3,2,3,3,3,3,2,3 };
  4. //数组中全是相同的值
  5. vector<int> a3 = { 2,2,2,2,2,2,2,2 };
复制代码
leetcode:排序数组
对于以上OJ题,传统的hoare和lomuto(前后指针法)的方法,过不了这个标题。堆排序和归并和希尔是可以过的,其它几个O(N^2)也不过的,由于这个题的测试用例中不仅仅有数据很多的大数组,也有一些特殊数据的数组,如大量重复数据的数组。堆排序和归并和希尔不是很受数据样本的分布和形态的影响,但是快排会,由于快排要选key,每次key都当趟分割都很偏,就会出现效率退化问题。
  1. //hoare版本
  2. class Solution
  3. {
  4. public:
  5.     void QuickSort(vector<int>& nums, int left, int right)
  6.     {
  7.         if(left >= right) return;
  8.         int key = nums[left];
  9.         int begin = left, end = right;
  10.         while(begin < end)
  11.         {
  12.             while(begin < end && nums[end] >= key) end--;
  13.             while(begin < end && nums[begin] <= key) begin++;
  14.             swap(nums[begin], nums[end]);
  15.         }
  16.         swap(nums[left], nums[begin]);
  17.         //递归左右区间
  18.         //[left, begin - 1] [begin, end] [end + 1, right]
  19.         QuickSort(nums, left, begin - 1);
  20.         QuickSort(nums, end + 1, right);
  21.     }
  22.     vector<int> sortArray(vector<int>& nums)
  23.     {
  24.         QuickSort(nums, 0, nums.size() - 1);
  25.         return nums;
  26.     }
  27. };
复制代码

  1. //lomuto版本
  2. class Solution
  3. {
  4. public:
  5.     void QuickSort(vector<int>& nums, int left, int right)
  6.     {
  7.         if(left >= right) return;
  8.             int keyi = left;
  9.             int prev = left, cur = left + 1;
  10.             while(cur <= right)
  11.             {
  12.                     if(nums[cur] < nums[keyi])
  13.                     {
  14.                             prev++;
  15.                             swap(nums[cur], nums[prev]);
  16.                     }
  17.                     cur++;
  18.             }
  19.             swap(nums[prev], nums[keyi]);
  20.             keyi = prev;
  21.             //递归左右区间
  22.             //[left, keyi - 1] keyi [keyi + 1, right]
  23.             QuickSort(nums, left, keyi - 1);
  24.             QuickSort(nums, keyi + 1, right);
  25.     }
  26.     vector<int> sortArray(vector<int>& nums)
  27.     {
  28.         QuickSort(nums, 0, nums.size() - 1);
  29.         return nums;
  30.     }
  31. };
复制代码

从上面的的运行结果分析,无论是hoare,还是lomuto的前后指针法,面临key有大量重复时,划分都不是很抱负。纵然是三数取中和随机选key,都不能很好的解决这里的问题。但是三路划分算法,把跟key相等的值都划分到了中间,可以很好的解决这里的问题。
2.快排之三路划分

三路划分算法解析:当面临有大量跟key类似的值时,三路划分的焦点思想有点类似hoare的左右指针和lomuto的前后指针的结合。焦点思想是把数组中的数据分为三段【比key小的值】 【与key相等的值】【比key大的值】,所以叫做三路划分算法。结合下图,理解一下实现思想:

  • key 默认取 left 位置的值。
  • left 指向区间最左边,right 指向区间最后边,cur 指向 left+1 位置。
  • cur 遇到比 key 小的值后跟 left 位置交换,换到左边,left++,cur++。
  • cur 遇到比 key 大的值后跟 right 位置交换,换到右边,right–。
  • cur 遇到跟 key 相等的值后,cur++。
  • 直到 cur>right 结束。




  1. class Solution
  2. {
  3. public:
  4.     void QuickSort(vector<int>& nums, int left, int right)
  5.     {
  6.         if(left >= right) return;
  7.         //随机选key
  8.         int randi = left + (rand() % (right - left + 1)); //注意:加上left
  9.         int key = nums[randi];
  10.         swap(nums[left], nums[randi]);
  11.         //三路划分
  12.         int begin = left, end = right, cur = left + 1;
  13.         while(cur <= end)
  14.         {
  15.             if(nums[cur] < key) swap(nums[begin++], nums[cur++]);
  16.             else if(nums[cur] > key) swap(nums[cur], nums[end--]);
  17.             else cur++;
  18.         }
  19.         //递归左右区间
  20.         //[left, begin - 1] [begin, end] [end + 1, right]
  21.         QuickSort(nums, left, begin - 1);
  22.         QuickSort(nums, end + 1, right);
  23.     }
  24.     vector<int> sortArray(vector<int>& nums)
  25.     {
  26.         QuickSort(nums, 0, nums.size() - 1);
  27.         return nums;
  28.     }
  29. };
复制代码
3.快排之内省排序


  • C++STL中的sort中用的introspective sort(内省排序),introsort是由David Musser在1997年设计的排序算法
  • introsort是introspective sort采用了缩写,它的实现思路就是进行自我侦测和反省,快排递归深度太深(SGI STL中使用的是深度为2倍排序元素数目的对数值)那就说明在这种数据序列下,选key出现了问题,性能在快速退化,那么就不要再进行快排分割递归了,改换为堆排序进行排序。
introsort的快排:
  1. void Swap(int* x, int* y)
  2. {
  3.         int tmp = *x;
  4.         *x = *y;
  5.         *y = tmp;
  6. }
  7. void AdjustDown(int* a, int n, int parent)
  8. {
  9.         int child = parent * 2 + 1;
  10.         while (child < n)
  11.         {
  12.                 //选出左右孩子中大的那一个
  13.                 if (child + 1 < n && a[child + 1] > a[child])
  14.                 {
  15.                         ++child;
  16.                 }
  17.                 if (a[child] > a[parent])
  18.                 {
  19.                         Swap(&a[child], &a[parent]);
  20.                         parent = child;
  21.                         child = parent * 2 + 1;
  22.                 }
  23.                 else
  24.                 {
  25.                         break;
  26.                 }
  27.         }
  28. }
  29. void HeapSort(int* a, int n)
  30. {
  31.         //向下调整建堆:O(N)
  32.         for (int i = (n - 1 - 1) / 2; i >= 0; --i)
  33.         {
  34.                 AdjustDown(a, n, i);
  35.         }
  36.         //排序:O(N*logN)
  37.         int end = n - 1;
  38.         while (end > 0)
  39.         {
  40.                 Swap(&a[end], &a[0]);
  41.                 AdjustDown(a, end, 0);
  42.                 --end;
  43.         }
  44. }
  45. void InsertSort(int* a, int n)
  46. {
  47.         for (int i = 1; i < n; i++)
  48.         {
  49.                 int end = i - 1;
  50.                 int tmp = a[i];
  51.                 //将tmp插入到[0, end]区间中,保持有序
  52.                 while (end >= 0)
  53.                 {
  54.                         if (tmp < a[end])
  55.                         {
  56.                                 a[end + 1] = a[end];
  57.                                 --end;
  58.                         }
  59.                         else
  60.                         {
  61.                                 break;
  62.                         }
  63.                 }
  64.                 a[end + 1] = tmp;
  65.         }
  66. }
  67. void IntroSort(int* a, int left, int right, int depth, int defaultDepth)
  68. {
  69.         if (left >= right)
  70.                 return;
  71.         //数组长度小于16的小数组,换为插入排序,简单递归次数
  72.         if (right - left + 1 < 16)
  73.         {
  74.                 InsertSort(a + left, right - left + 1);
  75.                 return;
  76.         }
  77.         //当深度超过2*logN时改用堆排序
  78.         if (depth > defaultDepth)
  79.         {
  80.                 HeapSort(a + left, right - left + 1);
  81.                 return;
  82.         }
  83.         depth++;
  84.         int begin = left;
  85.         int end = right;
  86.         //随机选key
  87.         int randi = left + (rand() % (right - left));
  88.         Swap(&a[left], &a[randi]);
  89.         int prev = left;
  90.         int cur = prev + 1;
  91.         int keyi = left;
  92.         while (cur <= right)
  93.         {
  94.                 if (a[cur] < a[keyi] && ++prev != cur)
  95.                 {
  96.                         Swap(&a[prev], &a[cur]);
  97.                 }
  98.                 ++cur;
  99.         }
  100.         Swap(&a[prev], &a[keyi]);
  101.         keyi = prev;
  102.         //[begin, keyi-1] keyi [keyi+1, end]
  103.         IntroSort(a, begin, keyi - 1, depth, defaultDepth);
  104.         IntroSort(a, keyi + 1, end, depth, defaultDepth);
  105. }
  106. void QuickSort(int* a, int left, int right)
  107. {
  108.         int depth = 0;
  109.         int logn = 0;
  110.         int N = right - left + 1;
  111.         for (int i = 1; i < N; i *= 2)
  112.         {
  113.                 logn++;
  114.         }
  115.         //自省排序
  116.         IntroSort(a, left, right, depth, logn * 2);
  117. }
  118. int* sortArray(int* nums, int numsSize, int* returnSize)
  119. {
  120.         srand(time(0));
  121.         QuickSort(nums, 0, numsSize - 1);
  122.         *returnSize = numsSize;
  123.        
  124.         return nums;
  125. }
复制代码
二.归并

1.外排序


  • 外排序(External sorting)是指能够处理极大量数据的排序算法。通常来说,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据构造为多个有序的临时文件。然后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。
  • 跟外排序对应的就是内排序,我们之前讲的常见的排序,都是内排序,它们排序思想顺应的是数据在内存中,支持随机访问。归并排序的思想不需要随机访问数据,只需要依次按序列读取数据,所以归并排序既是一个内排序,也是一个外排序。

2.文件归并排序


  • 读取n个值排序后写到file1,再读取n个值排序后写到file2。
  • file1和file2使用归并排序的思想,依次读取比较,取小的尾插到mfile,mfile归并为一个有序文件。
  • 将file1和file2删掉,mfile重定名为file1。
  • 再次读取n个数据排序后写到file2。
  • 继承走file1和file2归并,重复步调2,直到文件中无法读出数据。最后归并出的有序数据放到了file1中。



  1. //创建N个随机数,写到文件中
  2. void CreateNDate()
  3. {
  4.         const int n = 10000000;
  5.         srand(time(0));
  6.         const char* file = "data.txt";
  7.         FILE* fin = fopen(file, "w");
  8.         if (fin == NULL)
  9.         {
  10.                 perror("fopen error");
  11.                 return;
  12.         }
  13.         for (int i = 0; i < n; i++)
  14.         {
  15.                 int x = rand() + i;
  16.                 fprintf(fin, "%d\n", x);
  17.         }
  18.         fclose(fin);
  19. }
  20. //qsort用的函数指针
  21. int compare(const void* a, const void* b)
  22. {
  23.         return (*(int*)a - *(int*)b);
  24. }
  25. //读取数据后先排序再写到文件中,返回实际读取数据的个数,没有数据则返回0
  26. int ReadNDataSortToFile(FILE* fout, int n, const char* file1)
  27. {
  28.         //开辟数组
  29.         int x = 0;
  30.         int* a = (int*)malloc(sizeof(int) * n);
  31.         if (a == NULL)
  32.         {
  33.                 perror("malloc error");
  34.                 return 0;
  35.         }
  36.         //读取N个数据到数组中,如果遇到文件结束,实际读到j个
  37.         int j = 0;
  38.         for (int i = 0; i < n; i++)
  39.         {
  40.                 if (fscanf(fout, "%d", &x) == EOF)
  41.                         break;
  42.                 a[j++] = x;
  43.         }
  44.         if (j == 0)
  45.         {
  46.                 free(a);
  47.                 return 0;
  48.         }
  49.         //内排序
  50.         qsort(a, j, sizeof(int), compare);
  51.         //创建文件
  52.         FILE* fin = fopen(file1, "w");
  53.         if (fin == NULL)
  54.         {
  55.                 free(a);
  56.                 perror("fopen error");
  57.                 return 0;
  58.         }
  59.         //写排序后的数据到file1文件中
  60.         for (int i = 0; i < j; i++)
  61.         {
  62.                 fprintf(fin, "%d\n", a[i]);
  63.         }
  64.         free(a);
  65.         fclose(fin);
  66.         return j;
  67. }
  68. //将file1与file2中的数据归并到mfile文件中
  69. void MergeFile(const char* file1, const char* file2, const char* mfile)
  70. {
  71.         FILE* fout1 = fopen(file1, "r");
  72.         if (fout1 == NULL)
  73.         {
  74.                 perror("fopen error");
  75.                 return;
  76.         }
  77.         FILE* fout2 = fopen(file2, "r");
  78.         if (fout2 == NULL)
  79.         {
  80.                 perror("fopen error");
  81.                 return;
  82.         }
  83.         FILE* mfin = fopen(mfile, "w");
  84.         if (mfin == NULL)
  85.         {
  86.                 perror("fopen error");
  87.                 return;
  88.         }
  89.         int x1 = 0, x2 = 0;
  90.         int ret1 = fscanf(fout1, "%d", &x1);
  91.         int ret2 = fscanf(fout2, "%d", &x2);
  92.         while (ret1 != EOF && ret2 != EOF)
  93.         {
  94.                 if (x1 < x2)
  95.                 {
  96.                         fprintf(mfin, "%d\n", x1);
  97.                         ret1 = fscanf(fout1, "%d", &x1);
  98.                 }
  99.                 else
  100.                 {
  101.                         fprintf(mfin, "%d\n", x2);
  102.                         ret2 = fscanf(fout2, "%d", &x2);
  103.                 }
  104.         }
  105.         while (ret1 != EOF)
  106.         {
  107.                 fprintf(mfin, "%d\n", x1);
  108.                 ret1 = fscanf(fout1, "%d", &x1);
  109.         }
  110.         while (ret2 != EOF)
  111.         {
  112.                 fprintf(mfin, "%d\n", x2);
  113.                 ret2 = fscanf(fout2, "%d", &x2);
  114.         }
  115.         fclose(fout1);
  116.         fclose(fout2);
  117.         fclose(mfin);
  118. }
  119. int main()
  120. {
  121.         CreateNDate();
  122.         const char* file1 = "file1.txt";
  123.         const char* file2 = "file2.txt";
  124.         const char* mfile = "mfile.txt";
  125.         FILE* fout = fopen("data.txt", "r");
  126.         if (fout == NULL)
  127.         {
  128.                 perror("fopen error");
  129.                 return 0;
  130.         }
  131.         int m = 1000000;
  132.         ReadNDataSortToFile(fout, m, file1);
  133.         ReadNDataSortToFile(fout, m, file2);
  134.         while (1)
  135.         {
  136.                 MergeFile(file1, file2, mfile);
  137.                 //删除file1和file2
  138.                 remove(file1);
  139.                 remove(file2);
  140.                 //将mfile重命名为file1
  141.                 rename(mfile, file1);
  142.                 //继续读取数据,先排序,再写到file2中:若一个数据都读不到时,说明归并结束,结果在file1中
  143.                 if (ReadNDataSortToFile(fout, m, file2) == 0)
  144.                         break;
  145.         }
  146.         fclose(fout);
  147.         return 0;
  148. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

欢乐狗

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表