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

发布于:2025-02-10 ⋅ 阅读:(63) ⋅ 点赞:(0)

一.快排

1.快排性能分析

决定快排性能的关键点是每次单趟排序后,key对数组的分割,如果每次选key基本二分居中,那么快排的递归树就是颗均匀的满二叉树,性能最佳。但是实践中虽然不可能每次都是二分居中,但是性能也还是可控的。如果出现每次选到最小值/最大值,划分为0个和N-1的子问题时,时间复杂度为O(N^2),数组序列有序时就会出现这样的问题,在《八大排序》已经用三数取中或者随机选key解决了这个问题,也就是说我们解决了绝大多数的问题,但是现在还是有一些场景没解决(数组中有大量重复数据时),类似以下数据。

//数组中有多个跟key相等的值
vector<int> a1 = { 6,1,7,6,6,6,4,9 };
vector<int> a2 = { 3,2,3,3,3,3,2,3 };

//数组中全是相同的值
vector<int> a3 = { 2,2,2,2,2,2,2,2 };

leetcode:排序数组

对于以上OJ题,传统的hoare和lomuto(前后指针法)的方法,过不了这个题目。堆排序和归并和希尔是可以过的,其它几个O(N^2)也不过的,因为这个题的测试用例中不仅仅有数据很多的大数组,也有一些特殊数据的数组,如大量重复数据的数组。堆排序和归并和希尔不是很受数据样本的分布和形态的影响,但是快排会,因为快排要选key,每次key都当趟分割都很偏,就会出现效率退化问题。

//hoare版本
class Solution 
{
public:
    void QuickSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return;

        int key = nums[left];
        int begin = left, end = right;

        while(begin < end)
        {
            while(begin < end && nums[end] >= key) end--;
            while(begin < end && nums[begin] <= key) begin++;
            swap(nums[begin], nums[end]);
        }
        swap(nums[left], nums[begin]);

        //递归左右区间
        //[left, begin - 1] [begin, end] [end + 1, right]
        QuickSort(nums, left, begin - 1);
        QuickSort(nums, end + 1, right);
    }

    vector<int> sortArray(vector<int>& nums) 
    {
        QuickSort(nums, 0, nums.size() - 1);
        return nums;
    }
};

在这里插入图片描述

//lomuto版本
class Solution 
{
public:
    void QuickSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return;

	    int keyi = left;
	    int prev = left, cur = left + 1;
	    while(cur <= right)
	    {
		    if(nums[cur] < nums[keyi])
		    {
			    prev++;
			    swap(nums[cur], nums[prev]);
		    }
		    cur++;
	    }
	    swap(nums[prev], nums[keyi]);
	    keyi = prev;

	    //递归左右区间
	    //[left, keyi - 1] keyi [keyi + 1, right]
	    QuickSort(nums, left, keyi - 1);
	    QuickSort(nums, keyi + 1, right);
    }

    vector<int> sortArray(vector<int>& nums) 
    {
        QuickSort(nums, 0, nums.size() - 1);
        return nums;
    }
};

在这里插入图片描述

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

2.快排之三路划分

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

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution 
{
public:
    void QuickSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return;

        //随机选key
        int randi = left + (rand() % (right - left + 1)); //注意:加上left
        int key = nums[randi];
        swap(nums[left], nums[randi]);

        //三路划分
        int begin = left, end = right, cur = left + 1;
        while(cur <= end)
        {
            if(nums[cur] < key) swap(nums[begin++], nums[cur++]);
            else if(nums[cur] > key) swap(nums[cur], nums[end--]);
            else cur++;
        }

        //递归左右区间
        //[left, begin - 1] [begin, end] [end + 1, right]
        QuickSort(nums, left, begin - 1);
        QuickSort(nums, end + 1, right);
    }

    vector<int> sortArray(vector<int>& nums) 
    {
        QuickSort(nums, 0, nums.size() - 1);
        return nums;
    }
};

3.快排之内省排序

  1. C++STL中的sort中用的introspective sort(内省排序),introsort是由David Musser在1997年设计的排序算法
  2. introsort是introspective sort采用了缩写,它的实现思路就是进行自我侦测和反省,快排递归深度太深(SGI STL中使用的是深度为2倍排序元素数量的对数值)那就说明在这种数据序列下,选key出现了问题,性能在快速退化,那么就不要再进行快排分割递归了,改换为堆排序进行排序。

introsort的快排:

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//选出左右孩子中大的那一个
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//向下调整建堆:O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	//排序:O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[end], &a[0]);
		AdjustDown(a, end, 0);
		--end;
	}
}

void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		int end = i - 1;
		int tmp = a[i];
		//将tmp插入到[0, end]区间中,保持有序
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

void IntroSort(int* a, int left, int right, int depth, int defaultDepth)
{
	if (left >= right)
		return;

	//数组长度小于16的小数组,换为插入排序,简单递归次数
	if (right - left + 1 < 16)
	{
		InsertSort(a + left, right - left + 1);
		return;
	}

	//当深度超过2*logN时改用堆排序
	if (depth > defaultDepth)
	{
		HeapSort(a + left, right - left + 1);
		return;
	}
	depth++;
	int begin = left;
	int end = right;

	//随机选key
	int randi = left + (rand() % (right - left));
	Swap(&a[left], &a[randi]);
	int prev = left;
	int cur = prev + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;

	//[begin, keyi-1] keyi [keyi+1, end]
	IntroSort(a, begin, keyi - 1, depth, defaultDepth);
	IntroSort(a, keyi + 1, end, depth, defaultDepth);
}

void QuickSort(int* a, int left, int right)
{
	int depth = 0;
	int logn = 0;
	int N = right - left + 1;
	for (int i = 1; i < N; i *= 2)
	{
		logn++;
	}

	//自省排序
	IntroSort(a, left, right, depth, logn * 2);
}

int* sortArray(int* nums, int numsSize, int* returnSize) 
{
	srand(time(0));
	QuickSort(nums, 0, numsSize - 1);
	*returnSize = numsSize;
	
	return nums;
}

二.归并

1.外排序

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

在这里插入图片描述

2.文件归并排序

  1. 读取n个值排序后写到file1,再读取n个值排序后写到file2。
  2. file1和file2利用归并排序的思想,依次读取比较,取小的尾插到mfile,mfile归并为一个有序文件。
  3. 将file1和file2删掉,mfile重命名为file1。
  4. 再次读取n个数据排序后写到file2。
  5. 继续走file1和file2归并,重复步骤2,直到文件中无法读出数据。最后归并出的有序数据放到了file1中。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

//创建N个随机数,写到文件中
void CreateNDate()
{
	const int n = 10000000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; i++)
	{
		int x = rand() + i;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

//qsort用的函数指针
int compare(const void* a, const void* b)
{
	return (*(int*)a - *(int*)b);
}

//读取数据后先排序再写到文件中,返回实际读取数据的个数,没有数据则返回0
int ReadNDataSortToFile(FILE* fout, int n, const char* file1)
{
	//开辟数组
	int x = 0;
	int* a = (int*)malloc(sizeof(int) * n);
	if (a == NULL)
	{
		perror("malloc error");
		return 0;
	}

	//读取N个数据到数组中,如果遇到文件结束,实际读到j个
	int j = 0;
	for (int i = 0; i < n; i++)
	{
		if (fscanf(fout, "%d", &x) == EOF)
			break;

		a[j++] = x;
	}

	if (j == 0)
	{
		free(a);
		return 0;
	}

	//内排序
	qsort(a, j, sizeof(int), compare);

	//创建文件
	FILE* fin = fopen(file1, "w");
	if (fin == NULL)
	{
		free(a);
		perror("fopen error");
		return 0;
	}

	//写排序后的数据到file1文件中
	for (int i = 0; i < j; i++)
	{
		fprintf(fin, "%d\n", a[i]);
	}

	free(a);
	fclose(fin);

	return j;
}

//将file1与file2中的数据归并到mfile文件中
void MergeFile(const char* file1, const char* file2, const char* mfile)
{
	FILE* fout1 = fopen(file1, "r");
	if (fout1 == NULL)
	{
		perror("fopen error");
		return;
	}

	FILE* fout2 = fopen(file2, "r");
	if (fout2 == NULL)
	{
		perror("fopen error");
		return;
	}

	FILE* mfin = fopen(mfile, "w");
	if (mfin == NULL)
	{
		perror("fopen error");
		return;
	}

	int x1 = 0, x2 = 0;
	int ret1 = fscanf(fout1, "%d", &x1);
	int ret2 = fscanf(fout2, "%d", &x2);

	while (ret1 != EOF && ret2 != EOF)
	{
		if (x1 < x2)
		{
			fprintf(mfin, "%d\n", x1);
			ret1 = fscanf(fout1, "%d", &x1);
		}
		else
		{
			fprintf(mfin, "%d\n", x2);
			ret2 = fscanf(fout2, "%d", &x2);
		}
	}
	while (ret1 != EOF)
	{
		fprintf(mfin, "%d\n", x1);
		ret1 = fscanf(fout1, "%d", &x1);
	}
	while (ret2 != EOF)
	{
		fprintf(mfin, "%d\n", x2);
		ret2 = fscanf(fout2, "%d", &x2);
	}

	fclose(fout1);
	fclose(fout2);
	fclose(mfin);
}

int main()
{
	CreateNDate();

	const char* file1 = "file1.txt";
	const char* file2 = "file2.txt";
	const char* mfile = "mfile.txt";

	FILE* fout = fopen("data.txt", "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return 0;
	}

	int m = 1000000;
	ReadNDataSortToFile(fout, m, file1);
	ReadNDataSortToFile(fout, m, file2);

	while (1)
	{
		MergeFile(file1, file2, mfile);

		//删除file1和file2
		remove(file1);
		remove(file2);

		//将mfile重命名为file1
		rename(mfile, file1);

		//继续读取数据,先排序,再写到file2中:若一个数据都读不到时,说明归并结束,结果在file1中
		if (ReadNDataSortToFile(fout, m, file2) == 0)
			break;
	}

	fclose(fout);

	return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到