在常见排序算法中,直接选择排序和堆排序都是选择排序的一种,在这篇文章,我将用C语言详细解读直接选择排序和堆排序。
目录
直接选择排序排序
时间复杂度
O (n^2)
排序思想
直接选择排序排序的思想十分简单,常见的思想是找出最大或者最小的元素,将其放到相应位置(升序最大值放末尾,降序最大值放开头),循环找到最大值,直到所有元素都放到相应位置
排序实例
以下是一趟直接选择排序的过程,注意我一趟会找到一个最大值和一个最小值,并且放到相应位置,而不是常见的只找最大值。
这里存在一个重叠问题,当最大值与最终与相应位置重合时,会产生一些错误,在交换函数Swap中应注意这种情况。
代码实现
//交换两个元素的位置
void Swap(int* a, int* b)
{
if (*a == *b)//如果交换的两个位置重叠
{
return;
}
(*a) = (*a) + (*b);
(*b) = (*a) - (*b);
(*a) = (*a) - (*b);
}
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int min = left;
int max = left;
for (int i = left; i <= right; i++)//遍历选择最大值和最小值
{
if (a[i] > a[max])
{
max = i;
}
if (a[i] < a[min])
{
min = i;
}
}
if (a[max] == a[min])
{
return;
}
Swap(a + left, a + min);//将最大值和最小值放到相应位置
Swap(a + right, a + max);
left++;
right--;
}
}
堆排序
时间复杂度
O (nlogn)
数组与堆的对应关系
在数组存储堆时,如下
如元素和其两个孩子之间数组索引值的关系如上
排序思想
堆排序的思想与堆这个结构息息相关,堆是一种特殊的完全二叉树,我们首先来认识一下堆这个结构
大堆:树中的结点大于其左右孩子
小堆:树中的结点小于其左右孩子
堆排序步骤
1.生成堆(建堆)
建堆有两种方法
1.向上调整法
步骤:从第二个元素到最后一个元素循环向上建堆
向上建堆即比较此元素与其父结点的大小,
建大堆时如果此元素大于父节点值,交换此元素和父节点的值
建小堆时如果此元素小于父节点值,交换此元素和父节点的值
代码实现:
void Swap(int* a, int* b)
{
if (*a == *b)
{
return;
}
(*a) = (*a) + (*b);
(*b) = (*a) - (*b);
(*a) = (*a) - (*b);
}
//向上调整法将数组建大堆
void UpHeap(ElemType* arr, int size)
{
for (int i = 1; i < size; i++)
{
int child = i;
int parent= (i - 1)/2;
while (arr[child] > arr[parent])
{
Swap(arr + child, arr + parent);
child = parent;
if (child==0)
{
break;
}
parent = (child - 1) / 2;
}
}
}
//向上调整法将数组建小堆
void UpHeap2(ElemType* arr, int size)
{
for (int i = 1; i < size; i++)
{
int child = i;
int parent = (i - 1) / 2;
while (arr[child] < arr[parent])
{
Swap(arr + child, arr + parent);
child = parent;
if (child == 0)
{
break;
}
parent = (child - 1) / 2;
}
}
}
2.向下调整法(效率大于向上调整法)
步骤:从最后一个非叶子结点往上依次循环向下建堆
经过观察我们可以发现,最后一个非叶子结点的索引一定是n/2-1,n为数组元素个数
向下建堆:寻找孩子结点的较大值与父节点值比较
建大堆时如果此较大值大于父节点值,交换此元素和父节点的值
建小堆时如果此较大值小于父节点值,交换此元素和父节点的值
代码实现:
void Swap(int* a, int* b)
{
if (*a == *b)
{
return;
}
(*a) = (*a) + (*b);
(*b) = (*a) - (*b);
(*a) = (*a) - (*b);
}
//向下调整法建大堆
void DownHeap(ElemType* arr, int size)
{
for (int i = size / 2 - 1; i >= 0; i--)//从最后一个非叶子结点开始遍历
{
int child = i*2+1; //左孩子的数组索引
int parent = i; //父节点的数组索引
while (child < size )
{
if(arr[child+1]> arr[child] && child + 1 < size)//如果右孩子的值更大
{
child = child +1 ; //较大值孩子改为右孩子
}
if (arr[child] > arr[parent])//如果孩子大于父亲
{
Swap(arr + child, arr + parent);
parent = child;
child= parent * 2 + 1;
}
else
{
break;
}
}
}
}
//向下调整法建小堆
void DownHeap2(ElemType* arr, int size)
{
for (int i = size / 2 - 1; i >= 0; i--)
{
int child = i * 2 + 1;
int parent = i;
while (child < size)
{
if (arr[child + 1] < arr[child] && child + 1 < size)
{
child = child+1;
}
if (arr[child] < arr[parent])
{
Swap(arr + child, arr + parent);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
}
2.交换值
交换值的步骤十分简单,当时要注重策略,如升序排序时,我们采用建大堆的方法
建立大堆后,我们知道此时根节点一定是最大的,我们交换它和最后一个元素的值,此时根节点到达其最终位置,然后将剩余元素重新生成堆,重复这两个步骤,最终就可以形成升序序列。
到这里有些同学可能会说,为什么不是采用建小堆的方法?
倘若我们采用建小堆的方法,根节点一定是最小值,我们无需再交换它的位置,只需要将剩余元素重新建小堆,当时这个是剩余元素已经完全乱了,重新建堆要消耗大量时间,而在建大堆方法中,只有最后一个元素交换到根节点,意思就是说,只有一个元素不符合堆的规则,即交换到根节点的这个元素重新建堆时间消耗也很小,同理我们可以推断降序排列的情况,所以说升序排序用大堆,降序排序用小堆,可以大大节省时间。
实现代码:
//交换值函数
void Swap(int* a, int* b)
{
if (*a == *b)
{
return;
}
(*a) = (*a) + (*b);
(*b) = (*a) - (*b);
(*a) = (*a) - (*b);
}
// 堆排序
void AdjustDwon(int* a, int n, int root)//向下调整函数
{
int parent = root;
int left = 2 * parent + 1;
int right = 2 * parent + 2;
while (left<n)
{
int max = 0;
if (left >= n)
{
return;
}
if (right >= n)
{
max = left;
}
else
{
max = a[left] > a[right] ? left : right;
}
if (a[max] > a[parent])
{
Swap(a + max, a + parent);
parent = max;
}
else
{
break;
}
left = 2 * parent + 1;
right = 2 * parent + 2;
}
}
void HeapSort(int* a, int n)
{
for (int i = n / 2 - 1; i >= 0; i--)//先向下建堆
{
AdjustDwon(a, n, 0);
}
while (n)
{
Swap(a, a + n - 1); //交换最大值和最后一个元素的值
AdjustDwon(a, n, 0); //重新向下建堆
n--; //最后一个元素位置往前挪动一位
}
}