题外话
很多人都认为(包括过去的笔者),排序算法各种编程语言都有实现,我们就没必要再去造轮子了,所以再更多的时候就会忽略它们的应用场景和思想。这里,笔者的观点是,改造的轮子还是要造的。
有一个这样的问题(后续就称呼为TopK问题):给定一个数组,返回前K个最小值,返回的数组顺序没有要求,当然,K小于数组长度。这其实是LeetCode上一道标着“简单”的题,然后很多网友直接调一个排序接口就搞定了(包括笔者),效率还算高。但看到官方题解用了三种方法,分别是直接排序的,快排思想和堆的思想,突然发现我可能是误解官方题目的意思了,哈哈。
其实在这里,用快排和堆排序的思想就很好,一般来说我们直接排序的算法复杂度为O(nlogn),而另两种的复杂度为O(nlogk),先来说说快排吧。
快速排序
我们都知道快排的思想其实是分治,把一个待解决问题,拆成若干个子问题。快排通过不断选取参考值来划分左右,直到不能划分为止。附上C++的实现:
void quickSort(int arr[],int startIndex,int endIndex)
{
if(startIndex == endIndex)
{
return;
}
int pivotIndex = startIndex;
std::cout<<"startIndex->"<<startIndex<<" endIndex->"<<endIndex<<std::endl;
for(int i = startIndex + 1;i < endIndex; ++i)
{
if(arr[i] < arr[startIndex])
{
swap(arr[++pivotIndex],arr[i]);
}
}
std::cout<<"pivotIndex->"<<pivotIndex<<std::endl;
if(pivotIndex == startIndex && pivotIndex < endIndex)
{
quickSort(arr,pivotIndex + 1,endIndex);
}
else
{
swap(arr[pivotIndex],arr[startIndex]);
quickSort(arr,startIndex,pivotIndex);
quickSort(arr,pivotIndex,endIndex);
}
}
在这里,针对TopK问题,因为并不需要分割点左右两边都是有序的,所以我们其实只要找到分割点为K就行了。代码如下:
void topKByQuickSort(int arr[],int startIndex,int endIndex,int k)
{
if(startIndex == endIndex)
{
return;
}
int pivotIndex = startIndex;
std::cout<<"startIndex->"<<startIndex<<" endIndex->"<<endIndex<<std::endl;
for(int i = startIndex + 1;i < endIndex; ++i)
{
if(arr[i] < arr[startIndex])
{
swap(arr[++pivotIndex],arr[i]);
}
}
swap(arr[pivotIndex],arr[startIndex]);
if(pivotIndex == startIndex && pivotIndex < endIndex)
{
++pivotIndex;
}
std::cout<<"pivotIndex->"<<pivotIndex<<std::endl;
if(pivotIndex < k)
{
topKByQuickSort(arr,pivotIndex,endIndex);
}
else if(pivotIndex > k)
{
topKByQuickSort(arr,startIndex,pivotIndex);
}
}
其实还可以优化的,当K超过数组长度的二分之一时,我们完全可以取反,可以转化为找前Len - k 的最大值问题,这样就能保证TopK问题的复杂始终不超过O(log n/2)。
堆排序
堆排序主要是利用堆这个数据结构的特性——堆其实就是一个数组实现的完全二叉树,堆中所有父节点的值均大于子节点。由于这个特性,我们可以利用堆来做很多事,堆排序就是一个经典例子。因为堆的特性,堆顶节点的值要么是最小的,要么是最大的(这跟构建的堆类型有关,有最小堆和最大堆),所以,我们就可以不断弹出堆顶元素并更新堆,直至堆的长度变为0,这样就达到了排序的目的。然后这里的TopK问题跟堆排序的思想基本一致。附上C++实现代码:
#include <iostream>
#include<vector>
int heapSize = 0;
int getLeftIndex(int index)
{
return (index << 1) + 1;
}
int getRightIndex(int index)
{
return (index << 1) + 2;
}
void swapValue(int& a,int& b)
{
int temp = a;
a = b;
b = temp;
}
void adjugeHeap(std::vector<int> &vc,int index)
{
int leftIndex = getLeftIndex(index);
int lessIndex = index;
if(leftIndex < heapSize && vc[leftIndex] < vc[lessIndex])
{
lessIndex = leftIndex;
}
int rightIndex = getRightIndex(index);
if(rightIndex < heapSize && vc[rightIndex] < vc[lessIndex])
{
lessIndex = rightIndex;
}
if(lessIndex == index)
{
return;
}
else
{
swapValue(vc[lessIndex],vc[index]);
adjugeHeap(vc,lessIndex);
}
}
void buildHeap(std::vector<int>& vc)
{
heapSize = vc.size();
for(int i = (heapSize - 1) >> 1;i >= 0; --i)
{
adjugeHeap(vc,i);
}
}
int popHeap(std::vector<int>& vc)
{
if(heapSize == 0)
{
return -1;
}
int temp = vc[0];
vc[0] = vc[heapSize-- - 1];
adjugeHeap(vc,0);
return temp;
}
unsigned char deleteHeap(std::vector<int>& vc,int index)
{
unsigned char resCode = 0;
if(heapSize == 0 || index >= heapSize)
{
resCode = 1;
}
vc[index] = vc[heapSize-- - 1];
adjugeHeap(vc,index);
return resCode;
}
void insertHeap(std::vector<int>& vc,int value)
{
vc.push_back(value);
swapValue(vc[heapSize++],vc[vc.size() - 1]);
for(int i = (heapSize -1) >> 1; i >= 0; --i)
{
adjugeHeap(vc,i);
}
}
void heapSort(std::vector<int>& vc)
{
buildHeap(vc);
int temHeapSize = heapSize;
while(heapSize > 0)
{
swapValue(vc[heapSize-- -1],vc[0]);
adjugeHeap(vc,0);
}
heapSize = temHeapSize;
}
void topKbyHeap(std::vector<int>& vc,int k)
{
buildHeap(vc);
if(k > heapSize)
{
return;
}
for(int i = k;i < heapSize; ++ i)
{
if(vc[k] < vc[0])
{
swapValue(vc[k],vc[0]);
}
}
}
void logVc(std::vector<int> &vc)
{
for(int i = 0; i < vc.size(); ++i)
{
std::cout<<vc[i]<<"-";
}
std::cout<<std::endl;
}
这里,跟快排一样,堆排序思想解决TopK问题算法复杂度也是O(nlogk),k也可以始终小于n/2。
总结
算法是核心,编程是基础。