经典排序算法一、从简单选择排序到堆排序的深度解析
一、简单选择排序
1、从一个简单问题谈起
给定待排序序列A[ 1.....n ],选择出A中最小的记录。下面给出代码如下:
//选择待排序序列a中的最小记录,其下标为indexInt index = 0;for(int i=1;i<n;i++){if(a[i]<a[index])index=i;}
相信这段代码大家都能看懂,其中A[index]即为要找的最小记录。
2、简单选择排序的过程
问题描述:给定待排序序列A[ 1......n ] ,选择出第i小元素,并和A[i]交换,这就是一次简单选择排序。
//简单选择排序void SimpleSelectionSortOne(int *a,int n){int i,j,index;//1.进行n-1趟选择,每次选出第i小记录for(i=0;i<n-1;i++){index=i;//2.选择第i小记录为a[index]for(j=i+1;j<n;j++)if(a[j]<a[index])index=j;//3.与第i个记录交换if(index!=i){a[i]=a[i]+a[index];a[index]=a[i]-a[index];a[i]=a[i]-a[index];}}}
图解实例:
假设给定数组A[1......6]={ 2,5,7,9,5,2 },我们来分析一下A数组进行选择排序的过程
第一趟:i=0,index=0, 不用交换。得到序列:{ 2,5,7,9,5,2 }
第二趟:i=1,index=5, a[1] 和 a[5] 进行交换。得到序列:{ 2,2,7,9,5,5 }
第三趟:i=2,index=4, a[2] 和 a[4] 进行交换交换。得到序列:{ 2,2,5,9,7,5 }
第四趟:i=3,index=5, a[3] 和 a[5] 进行交换交换。得到序列:{ 2,2,5,5,7,9 }
第五趟:i=4,index=5, 不用交换。得到序列:{ 2,2,5,5,7,9 }
第六趟:趟选择结束,得到有序序列:{ 1,2,3,5,8,9 }
最佳情况下(待排序序列有序)记录移动次数为0,最坏情况下(待排序序列逆序)记录移动次数n-1。外层循环进行了n-1趟选择,第i趟选择要进行n-i次比较。每一趟的时间:n-i次的比较时间+移动记录的时间(为一常数0或1,可以忽略)。总共进行了n-1趟。忽略移动记录的时间,所以总时间为(n-1)*(n-i)=n^2-(i+1)*n+i。时间复杂度为O(n^2)。不管是最坏还是最佳情况下,比较次数都是一样的,所以简单选择排序平均时间、最坏情况、最佳情况 时间复杂度都为O(n^2)。
递归实现简单选择排序
//递归函数进行简单选择排序void SimpleSelectionSortTwo(int *a,int n){int index,i;if(n==0)return;//1.选择待排序序列a中的最小记录,其下标为indexfor(index=i=0;i<n;i++){if(a[i]<a[index])index=i;}//2.最小记录与待排序序列首元素进行交换if(index!=0){a[0]=a[0]+a[index];a[index]=a[0]-a[index];a[0]=a[0]-a[index];}//3.待排序序列元素个数减少,递归对剩下的无序序列排序simpleSelectionSortTwo(a+1,n-1);}
二、堆排序
从上文我们知道简单选择排序的时间复杂度为O(n^2),熟悉各种排序算法的朋友都知道,这个时间复杂度是很大的,所以怎样减小简单选择排序的时间复杂度呢?从上文分析中我们知道简单选择排序主要操作是进行关键字的比较,所以怎样减少比较次数就是改进的关键。 简单选择排序中第i趟需要进行n-i次比较,如果我们用到前面已排好的序列a[1...i-1]是否可以减少比较次数呢?答案是可以的。举个例子来说吧,A、B、C进行比赛,B战胜了A,C战胜了B,那么显然C可以战胜A,C和A就不用比了。
1、什么是堆
Min-heap: 父节点的值小于或等于子节点的值
Max-heap: 父节点的值大于或等于子节点的值
2、堆的存储
一般都用数组来表示堆,i结点的父结点下标就为(i–1)/2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。
由于堆存储在下标从0开始计数的数组中,因此,在堆中给定下标为i的结点时:
(1)如果i=0,结点i是根结点,无父结点;否则结点i的父结点为结点(i-1)/2;
(2)如果2i+1>n-1,则结点i无左子女;否则结点i的左子女为结点2i+1;
(3)如果2i+2>n-1,则结点i无右子女;否则结点i的右子女为结点2i+2。
3、大根堆插入元素
插入一个元素:新元素被加入到heap的末尾,然后更新树以恢复堆的次序。
每次插入都是将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一个数据并入到有序区间中。需要从下往上,与父节点的关键码进行比较,对调。
图解实例:
代码实例:
bool MaxHeap::Insert(const int iValue){if(m_iCurrentHeapSize == m_iHeapMaxSize){cout<<"[info] max heap full"<<endl;return false;}m_pHeap[m_iCurrentHeapSize] = iValue;ShiftUp(m_iCurrentHeapSize);m_iCurrentHeapSize++;return true;}
4、删除大根堆堆的最大元素
按定义,堆中每次都删除第0个数据。为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,堆的元素个数-1,然后再从根结点开始进行一次从上向下的调整。调整时先在左右儿子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于从根结点将一个数据的“下沉”过程。
图解实例:
代码实例:
bool MaxHeap::RemoveMaxValue(int &iValue){if(m_iCurrentHeapSize == 0){cout<<"[info] max heap empty"<<endl;return false;}iValue = m_pHeap[0];m_pHeap[0] = m_pHeap[m_iCurrentHeapSize-1];m_iCurrentHeapSize--;ShiftDowm(0, m_iCurrentHeapSize-1);return true;}
5、创建最大堆
对于叶子节点,不用调整次序,根据满二叉树的性质,叶子节点比内部节点的个数多1.所以i=n/2 -1 ,不用从n开始。就是从最后一个有叶子结点的结点开始。
代码实例:
MaxHeap::MaxHeap(int *pArry,int iSize){m_iHeapMaxSize = iSize > HEAPDEFAULTSIZE ? iSize : HEAPDEFAULTSIZE;try{m_pHeap = new int[m_iHeapMaxSize];}catch(std::bad_alloc){exit(-1);}m_iCurrentHeapSize = iSize;for(int i = 0; i < iSize; i++){m_pHeap[i] = pArry[i];}//craete max heapint iCurerentPos = (m_iCurrentHeapSize - 2)/2;while(iCurerentPos >= 0){ShiftDowm(iCurerentPos, m_iHeapMaxSize-1);iCurerentPos--;}}
6、堆排序
过程描述:1、建堆 2、将堆顶记录和堆中最后一个记录交换 3、筛选法调整堆,堆中记录个数减少一个,重复第2步。整个过程中堆是在不断的缩减。
代码实现:
bool MaxHeap::HeapSort(){if(m_iCurrentHeapSize == 0){cout<<"[info] max heap is empty"<<endl;return false;}for(int i=m_iCurrentHeapSize-1; i>0; i--){m_pHeap[0] = m_pHeap[0] + m_pHeap[i];m_pHeap[i] = m_pHeap[0] - m_pHeap[i];m_pHeap[0] = m_pHeap[0] - m_pHeap[i];ShiftDowm(0, i - 1);}cout<<"[info] max heap sort content is: "<<endl;PrintHeap();return true;}
三、编码测试
my_heap.h
/*@breif implement max heap@author wanglu@time 20200627*/const int HEAPDEFAULTSIZE = 50;class MaxHeap{public:/*@brief build heap*/MaxHeap(int iSize = HEAPDEFAULTSIZE);MaxHeap(int *pArry,int iSize);/*@brief insert into heap*/bool Insert(const int iValue);/*@brief remvove max value*/bool RemoveMaxValue(int &iValue);/*@brief heap sotr*/bool HeapSort();/*@brief output heap elements*/void PrintHeap();/*@brief get max heap capacity*/int GetMaxCapacity(){return m_iHeapMaxSize;}/*@brief get current max heap size*/int GetCurrentSize(){return m_iCurrentHeapSize;}protected:/*@brief Adjust downward heap*/void ShiftDowm(int start, int end);/*@brief Upward adjustment heap*/void ShiftUp(int start);private:int *m_pHeap;//Max Heap objectint m_iCurrentHeapSize;//heap current heap element sizeint m_iHeapMaxSize;//heap default max size};
my_heap.cpp
using namespace std;MaxHeap::MaxHeap(int iSize){m_iHeapMaxSize = iSize > HEAPDEFAULTSIZE ? iSize : HEAPDEFAULTSIZE;m_iCurrentHeapSize = 0;try{m_pHeap = new int[m_iHeapMaxSize];}catch(std::bad_alloc){exit(-1);}}MaxHeap::MaxHeap(int *pArry,int iSize){m_iHeapMaxSize = iSize > HEAPDEFAULTSIZE ? iSize : HEAPDEFAULTSIZE;try{m_pHeap = new int[m_iHeapMaxSize];}catch(std::bad_alloc){exit(-1);}m_iCurrentHeapSize = iSize;for(int i = 0; i < iSize; i++){m_pHeap[i] = pArry[i];}//craete max heapint iCurerentPos = (m_iCurrentHeapSize - 2)/2;while(iCurerentPos >= 0){ShiftDowm(iCurerentPos, m_iHeapMaxSize-1);iCurerentPos--;}}void MaxHeap::ShiftDowm(int start, int end){int iCur = start;int iMaxChirdPos = 2*iCur + 1;int iTempValue = m_pHeap[iCur];while(iMaxChirdPos<= end){if(iMaxChirdPos<end && m_pHeap[iMaxChirdPos]<m_pHeap[iMaxChirdPos+1]){iMaxChirdPos++;}if(m_pHeap[iCur] > m_pHeap[iMaxChirdPos]){break;}else{m_pHeap[iCur] = m_pHeap[iMaxChirdPos];m_pHeap[iMaxChirdPos] = iTempValue;iCur = iMaxChirdPos;iMaxChirdPos = 2*iCur + 1;}}m_pHeap[iCur] = iTempValue;}bool MaxHeap::Insert(const int iValue){if(m_iCurrentHeapSize == m_iHeapMaxSize){cout<<"[info] max heap full"<<endl;return false;}m_pHeap[m_iCurrentHeapSize] = iValue;ShiftUp(m_iCurrentHeapSize);m_iCurrentHeapSize++;return true;}void MaxHeap::ShiftUp(int start){int iCur = start;int iParentPos = (iCur -1)/2;int itempValue = m_pHeap[iCur];while(iCur > 0){if(itempValue < m_pHeap[iParentPos]){break;}else{m_pHeap[iCur] = m_pHeap[iParentPos];m_pHeap[iParentPos] = itempValue;iCur = iParentPos;iParentPos = (iCur -1)/2;}}m_pHeap[iCur] = itempValue;}bool MaxHeap::RemoveMaxValue(int &iValue){if(m_iCurrentHeapSize == 0){cout<<"[info] max heap empty"<<endl;return false;}iValue = m_pHeap[0];m_pHeap[0] = m_pHeap[m_iCurrentHeapSize-1];m_iCurrentHeapSize--;ShiftDowm(0, m_iCurrentHeapSize-1);return true;}void MaxHeap::PrintHeap(){for(int i=0; i<m_iCurrentHeapSize-1; i++){cout<<m_pHeap[i]<<" ";}cout<<m_pHeap[m_iCurrentHeapSize-1]<<endl;}bool MaxHeap::HeapSort(){if(m_iCurrentHeapSize == 0){cout<<"[info] max heap is empty"<<endl;return false;}for(int i=m_iCurrentHeapSize-1; i>0; i--){m_pHeap[0] = m_pHeap[0] + m_pHeap[i];m_pHeap[i] = m_pHeap[0] - m_pHeap[i];m_pHeap[0] = m_pHeap[0] - m_pHeap[i];ShiftDowm(0, i - 1);}cout<<"[info] max heap sort content is: "<<endl;PrintHeap();return true;}
堆排序测试HeapSortTest.cpp
using namespace std;int main(){int iArry[10] = {2,3,1,6,4,9,6,4,0,7};MaxHeap MyHeap(iArry, 10);MyHeap.PrintHeap();MyHeap.HeapSort();return 0;}
堆排序打印如下:
堆插入测试HeapInsertTest.cpp
using namespace std;int main(){int iArry[10] = {2,3,1,6,4,9,6,4,0,7};MaxHeap MyHeap(iArry, 10);MyHeap.PrintHeap();//MyHeap.HeapSort();MyHeap.Insert(6);MyHeap.Insert(8);MyHeap.PrintHeap();//int iValue;//MyHeap.RemoveMaxValue(iValue);//cout<<"[info] max heap remove max value is "<<iValue<<endl;//MyHeap.PrintHeap();return 0;}
堆插入打印如下:
堆删除测试HeapRemoveTest.cpp
using namespace std;int main(){int iArry[10] = {2,3,1,6,4,9,6,4,0,7};MaxHeap MyHeap(iArry, 10);//MyHeap.PrintHeap();//MyHeap.HeapSort();MyHeap.Insert(6);MyHeap.Insert(8);MyHeap.PrintHeap();int iValue;MyHeap.RemoveMaxValue(iValue);cout<<"[info] max heap remove max value is "<<iValue<<endl;MyHeap.PrintHeap();return 0;}
堆删除最大元素打印如下:
创建最大堆测试CreateHeapTest.cpp
using namespace std;int main(){int iArry[10] = {2,3,1,6,4,9,6,4,0,7};MaxHeap MyHeap(iArry, 10);cout<<"[info] create max heap is:"<<endl;MyHeap.PrintHeap();cout<<"[info] max heap max capacity is "<<MyHeap.GetMaxCapacity()<<endl;cout<<"[info] max heap current size is "<<MyHeap.GetCurrentSize()<<endl;return 0;}
创建最大堆打印如下:
四、时间复杂度分析
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。
