[排序算法二]选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
算法性能
- 时间复杂度:O(n^2),总循环次数 n(n-1)/2。数据交换次数 O(n),这点上来说比冒泡排序要好,因为冒泡是把数据一位一位的移上来,而选择排序只需要在子循环结束后移动一次即可。冒泡的极端情况下,交换次数是O(n^2)
- 稳定性:不稳定
为啥说这货不稳定涅
举个例子来说吧,有这样一个数组 [5, 2, 5],假设 5
就是 5
,这里加个星号是为了与第一个5区分出来。对这个数组使用选择排序,最后的结果是[2, 5* ,5]
,破坏了 5
这个元素在原数组中的排序,所以说这货是不稳定的。
算法步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
又盗图了,再次请求谅解...
上面算法步骤中有个词儿叫未排序序列
,这里稍微解释下:
假设有一个10个数字的数组需要排序,那初始时这10个数字就是未排序序列,
经过第一次循环,我们会把下标为[0]元素替换成数组中最小的数字,也就是说此元素已经被排序,那剩下的下标为[1-9]的9个数字才是未排序序列。
举个例子吧,数组 [1, 7, 4, 8]
:
- 下标为
0
的元素1
不动,遍历后面的数字7, 4, 8
,找到最小数字为4
,记录下它的下标为2
,因为1 < 4
,所以不做任何操作,数组依然是[1, 7, 4, 8]
- 下标为
1
的元素7
不动,遍历后面的数字4, 8
,找到最小数字为4
,记录下它的下标为2
,因为7 > 4
,所以把下标为1
和下标为2
的元素互换,数组变成[1, 4, 7, 8]
- 继续以上步骤...直到把所有元素遍历一遍
这样看下来,过程还是很简单的,就是两个嵌套,代码实现如下:
public void Sort1(int[] numbers)
{
var count1 = 0;
var count2 = 0;
var length = numbers.Length;
for (var i = 0; i < length; i++)
{
var minIndex = i;
for (var j = i + 1; j < length; j++)
{
if (numbers[j] < numbers[minIndex])
{
minIndex = j;
}
count1++;
}
if (minIndex != i)
{
var temp = numbers[i];
numbers[i] = numbers[minIndex];
numbers[minIndex] = temp;
count2++;
}
}
Console.WriteLine($"结果:{string.Join(",", numbers)};循环次数:{count1};交换次数:{count2}");
}
算法优化
选择排序的核心思路无外乎就是用未排序序列中的第一个元素和其他元素中最小的那个元素比较,如果第一个元素比较大,那就把两个元素互换。
发散一下,反正是一次遍历,如果同时把最小和最大两个元素找出来,分别跟未排序序列的第一个元素和最后一个元素比较,不是一次性就能对两个元素进行排序吗? 这样一来,遍历的次数也就只需要数组长度的一半即可,想想就让人兴奋。
对给定数组 [4, 7, 3, 8, 6]
进行第一次循环:
- 找到最小值
3
,将它与第一个元素互换[3, 7, 4, 8, 6]
- 找到最大值
8
,将它与最后一个元素互换[3, 7, 4, 6, 8]
这样在一次循环中,我们找到了数组的最小值和最大值,并将它们放置到了数组的相应位置。换句话说,数组首尾两个元素都进行了排序,此时,未排序序列变成了7, 4, 6
,接下来我们只需对这三个元素再进行排序即可。
当然,世事不会总那么一番风顺,上面的思路乍一看很OK,但其实还是有点小坑的。
因为要同时操作最大和最小两个元素,所以在一次遍历后我们需要两次互换的操作,一般顺序是先处理最小值,再处理最大值,那问题就来了,
当未排序序列的第一项比其他项都大 的情况下,如:[9, 8, 7, 6]
,我们一般思路是这样的:
- 第一个元素,下标为
0
值为9
- 声明两个变量,用于保存其余元素中的最小值及其下标,初始化都指向第一个元素
var min = 9; var minIndex = 0;
- 声明两个变量,用于保存其余元素中的最大值及其下标,初始化都指向第一个元素
var max = 9; var maxIndex = 0;
- 遍历
8, 7, 6
- 找到最小值为
6
下标为3
,所以更新最小值及其索引min = 6; minIndex = 3;
- 没有找到比
9
更大的元素,所以此时最大值及其索引依然是max = 9; maxIndex = 0;
- 先处理最小值,发现
9 > 6
,需要将两者(下标是[0]的元素和下标是[3]的元素)互换,互换之后数组变成 [6,8,7,9] - 接着处理最大值,我们的思路是,把下标是
maxIndex
的元素与最后一个元素互换,这时最大值的下标是0
,最后一个元素的下标是3
,如果直接将两者互换会发生什么情况呢?因为刚才第[0]个元素已经与第[3]个元素发生了互换,当前的数组已经是[6,8,7,9]
了,再换一次,又变回了 [9,8,7,6],这显然是不对的,事实上,当上一步结束后,最大值[9]的下标已经变成了minIndex
,所以这里要处理一下 maxIndex 的指向,将它指到 [9] 当前的位置上maxIndex = minIndex
罗里吧嗦一堆,自己看了都头昏,还是直接上代码吧
public void Sort3(int[] arr)
{
var len = arr.Length;
var left = 0;
var right = len - 1;
var count1 = 0;
var count2 = 0;
while (left < right)
{
int max = left;//记录无序区最大元素下标
int min = left;//记录无序区最小元素下标
int j = 0;
for (j = left + 1; j <= right; j++)
{
//找最大元素下标
if (arr[j] < arr[min])
{
min = j;
}
//找最小元素下标
if (arr[j] > arr[max])
{
max = j;
}
count1++;
}
//最小值如果是第一个则没有必要交换
if (min != left)
{
int tmp = arr[left];
arr[left] = arr[min];
arr[min] = tmp;
count2++;
}
// 手动高亮
// left 是第一个元素的下标,max是最大值的下标,
// left == max 说明第一个元素的值比剩余元素都大,这时就需要更新索引
if (max == left)
{
max = min;
}
//最大值如果是最后一个则没必要交换
if (max != right)
{
int tmp = arr[right];
arr[right] = arr[max];
arr[max] = tmp;
count2++;
}
left++;
right--;
}
Console.WriteLine($"结果:{string.Join(",", arr)};循环次数:{count1};交换次数:{count2}");
}
上面的代码是我从其他博客上抄的,因为感觉比我自己实现的要好一些,下面是我自己写的:
public void Sort2(int[] numbers)
{
var count1 = 0;
var count2 = 0;
for (var i = 0; i < numbers.Length / 2; i++)
{
var left = i;
var right = numbers.Length - i - 1;
var minIndex = left;
var maxIndex = left;
for (var j = left + 1; j <= right; j++)
{
if (numbers[j] < numbers[minIndex])
{
minIndex = j;
}
if (numbers[j] > numbers[maxIndex])
{
maxIndex = j;
}
count1++;
}
if (left != minIndex)
{
var temp = numbers[minIndex];
numbers[minIndex] = numbers[left];
numbers[left] = temp;
count2++;
}
// 如果第一个就是最大的,因为已经把他移动到minIndex上了,所以这里要更新下索引
if (left == maxIndex)
{
maxIndex = minIndex;
}
if (right != maxIndex)
{
var temp = numbers[right];
numbers[right] = numbers[maxIndex];
numbers[maxIndex] = temp;
count2++;
}
}
Console.WriteLine($"结果:{string.Join(",", numbers)};循环次数:{count1};交换次数:{count2}");
}
后面两个版本其实代码的运行结果是一致的,只是第三个版本看起来不如第二个直观,仅做记录,毕竟也是费了些脑力搞出来的,不拉出来溜溜感觉有点可惜~~手动捂脸