划分方法的应用

划分对于分治的算法设计很重要,下面就专门说一个划分的算法设计,然后再举例子说一些具体的应用。

int Partition(int data[], int start, int end)
{
	if(data == NULL)
		return -1;
    int index = RandomInRange(start, end);
    int small = start - 1;
 
    swap(data[index], data[end]);
 
    for(index = start; index < end; index++)
    {
        if(data[index] < data[end])
        {
            ++small;
            if(small != index)
            {
                swap(data[small], data[index]);
            }//if
        }//if
    }//for
 
    ++small;
    swap(data[small], data[end]);
 
    return small;
}//Partition()

  下面看一下划分的应用。

快速排序中的划分

void QuickSort(int data[], int start, int end)
{
    if(start == end)
        return;
 
    int index = Partition(data, start, end);
 
    if(index > start)
        QuickSort(data, start, index - 1);
    if(index < end)
	QuickSort(data, index + 1, end);
}

  递归的程序一定要有递归出口。

查找第K大的数字

int findKth(int *data, int start, int end, int k)
{	
	int pos = partition(data, start, end);
	if(pos + 1 == k)
		return data[pos];
	else if(pos + 1 > k)
		return findKth(data,start, pos -1, k);
	else
		return findKth(data, pos + 1, end, k);
}

int findKthLargest(int *nums, int numsSize, int k) 
{
    //转换成求第k小元素
   int m = numsSize - k +  1;

   int result = findKth(nums, 0, numsSize - 1, m);

   return result;
}

  

数组中出现次数超过一半的数字

这个问题貌似很难,但是设想如果这个数组已经排序好了,那么这个出现次数超过一半的数字肯定会出现在中位数的地方。所以可以利用上面的算法,找数组中的中位数小的元素。

但是测试用例一定要首先写好,要防止空的数组、数组中根本不存在超过一半多这样的数字,这次用循环来实现找第k小的元素,不用递归了。

int MoreThanHalfNum_Solution(vector<int> numbers) 
{
	if(numbers.size() == 0)
		return 0;
	
	int start = 0;
	int end = numbers.size() - 1;
	int middle = numbers.size() >> 1;

	int index = partition(numbers, start, end);

	while(index != middle)
	{
		if(index > middle)
		{
			end = index - 1;
			index = partition(numbers, start, end);
		}
		else
		{
			start = index + 1;
			index = partition(numbers, start, end);
		}
	}//while

	int result = numbers[middle];
	
	if(!checkMoreThanHalf(numbers,result))
	{
		return 0;
	}

	return result;
}

  注意上面的循环的方式,感觉比递归的方式更好。但是通过中位数找到的数字可能不是正确的答案,最后要验证一下,这个数字是不是出现的次数多于数组大小的一半。

bool checkMoreThanHalf(vector<int> &numbers, int result)
{
	int times = 0;
	for(int i = 0; i < numbers.size(); i++)
	{
		if(numbers[i] == result)
		{
			times++;
		}
	}//for
	
	if(times * 2 > numbers.size())
	{
		return true;
	}
	
	return false;
}

  注意上面的函数的返回值:正确的情况下,返回查找到的数据,错误的时候,返回错误码0。

不管怎么弄,上面的算法的实现都是基于划分算法来实现的,其实除了这种方法,还可以有其他的解决方案

设置两个变量,一个变量用于保存目前出现次数最多的那个数字,另一变量用来记录出现的次数。随着不断的遍历,每遍历一个数,如果和前面保存的数字相同,计数器就+1,不相同就-1。如果计数器减到0,就代表之前存的数已经被淹没掉了,这是要重新记录新的数字,同时置计数变量为1。既然结果数字多于数组个数的一半,那么最后记录中留下的肯定是这个结果数字。但是要注意,原数组中可能本来就没有多于一半的这样的数字,但是最后还是会有一个结果的,所以要最后校验一次。如果发现错误的话,及时的返回错误码0。

int MoreThanHalfNum(vector<int> numbers)
{
	int times = 1;
	int result = numbers[0];

	for(int i = 1; i < numbers.size(); i++)
	{
		if(numbers[i] == result)
		{
			times++;
		}
		else
		{
			times--;
			if(times == 0)
			{
				result = numbers[i];
				times = 1;
			}

		}//else
	}//for

	if(!checkMoreThanHalf(numbers, result))
	{
		return 0;
	}

	return result;
}

  对于查找数组中出现次数多于一半的数字,有这两种方法,他们的时间复杂度都是O(n),但是注意第一种改变了原来的数组,但是第二种没有改变原数组,是一种很好的方法。

 获取数组中的最小的K个数

这个题目第一次看到可能会想到先排序,然后就可以很简单的获得最小的k个数了,但是排序的时间复杂度是O(nlogn),这可能达不到算法时间复杂度的要求,所以又想到了划分。类似前面那个出现次数大于数组个数一半的题目,只要划分能够定位到k-1那个元素。那么[0...k-1]的这些数字就是最小的k个数字。

vector<int> GetKLeastNumbers(vector<int> input, int k)
{   
    vector<int> result;

    if(input.size() == 0 || k <= 0 || k > input.size())
        return result;
         
    int start = 0;
    int end = input.size() - 1;
         
    int index = partition(input, start, end);
    while(index != k - 1)
    {
        if(index > k -1)
        {
            end = index - 1;
            index = partition(input, start, end);
        }
        else
        {
            start = index + 1;
            index = partition(input, start, end);
        }
             
    }//while
         
    for(int i = 0; i < k; i++)
    {
        result.push_back(input[i]);
    }
         
    return result;
}

  但是这个算法会改变原来的数组,所以如果要求不改变原来的数组,得想另外的办法。

最小K个数的另一种解决的办法

我们可以另外建立一个容器,在这个容器中存储最小的K个数字。一次遍历原始的数字,如果这时K的容器数组没有满,直接把这个原始数据放到容器中,如果容器已经满了,则比较原始数组的值和容器中最大值,如果原始的数字小,则替换容器中的最大值。否则忽略这个原始的数据,继续向后遍历。

因此在容器满了之后要做下面3件事:

在K的容器中,找到最大值;可能会删除这个最大值;可能会插入一个新的值。

如果用一个二叉树完成上面的3个操作,时间复杂度就能够控制在O(logk),这样的话整个算法的时间复杂度控制在O(nlogk)。由于每次都是从容器中找到最大值,所以很容易考虑到最大堆,这样的话就可以在O(1)的时间找到容器的最大值,但是删除和插入的时间复杂度是O(logk)。

对于二叉树,可以选用set或者multiset来代替,因为这两个东西都是基于红黑树实现的,红黑树的查找、删除、插入的时间复杂度都是O(logk)。

typedef multiset<int, greater<int> > intSet;
typedef multiset<int, greater<int> >::iterator setIterator;

vector<int> GetKLeastNumbers(const vector<int> &data, int k)
{
	intSet leastNumbers;
	vector<int> result;

	int len = data.size();

	leastNumbers.clear();

	//检验边界值,增加代码的健壮性
	if(k < 1 || data.size() == 0 || data.size() < k)
	{
		return result;
	}

	for(int i = 0; i < len;i++)
	{
		if(leastNumbers.size() < k)
		{
			leastNumbers.insert(data[i]);
		}
		else
		{
			setIterator iterGreatest = leastNumbers.begin();

			if(data[i] < *(leastNumbers.begin()))
			{
				leastNumbers.erase(iterGreatest);
				leastNumbers.insert(data[i]);
			}
		}//else
	}//for

	//放到vector中返回数据
	for(setIterator iterK = leastNumbers.begin(); iterK != leastNumbers.end(); iterK++)
    {
        result.push_back(*iterK);
    }

	return result;

}

  这种算法的优点是它不会改动原来的数组,同时特别的适合海量的数据,因为可以不用一次把全部的数读取到内存中来,可以分批的读入内存。所以这种算法很适合n很大但是k很小的情况。

原文地址:https://www.cnblogs.com/stemon/p/4704539.html