图解冒泡
我们以[8,2,5,9,7]这组数字来做示例,上图来战:
我们从左往右依次冒泡,将小的往右移动
首先比较第一个数和第二个数的大小,我们发现2比8要小,那么保持原位,不做改动。位置还是8,2,5,9,7。
指针往右移动一格,接着比较:
比较第二个数和第三个数的大小,发现2比5要小,所以位置交换,交换后数组更新为:[8,5,2,9,7]。
指针再往右移动一格,继续比较:
比较第三个数和第四个数的大小,发现2比9要小,所以位置交换,交换后数组更新为:[8,5,9,2,7]
同样,指针再往右移动,继续比较:
比较第4个数和第5个数的大小,发现2比7要小,所以位置交换,交换后数组更新为:[8,5,9,7,2]
下一步,指针再往右移动,发现已经到底了,则本轮冒泡结束,处于最右边的2就是已经排好序的数字。
通过这一轮不断的对比交换,数组中最小的数字移动到了最右边。
接下来继续第二轮冒泡:
由于右边的2已经是排好序的数字,就不再参与比较,所以本轮冒泡结束,本轮冒泡最终冒到顶部的数字5也归于有序序列中,现在数组已经变化成了[8,9,7,5,2]。
让我们开始第三轮冒泡吧!
由于8比7大,所以位置不变,此时第三轮冒泡也已经结束,第三轮冒泡的最后结果是[9,8,7,5,2]
紧接着第四轮冒泡:
9和8比,位置不变,即确定了8进入有序序列,那么最后只剩下一个数字9,放在末尾,自此排序结束。
参考代码:
class Solution{
public:
//冒泡排序
void mp_sort(int* array, int length)
{
for (int i = 0; i < length; ++i) {
for (int j = 0; j < length-1; ++j) {
if (array[j] > array[j+1])
swap(array[j],array[j+1]);
}
}
}
};
图解选排
我们还是以[8,2,5,9,7]这组数字做例子。
第一次选择,先找到数组中最小的数字2,然后和第一个数字交换位置。(如果第一个数字就是最小值,那么自己和自己交换位置,也可以不做处理,就是一个if的事情)
第二次选择,由于数组第一个位置已经是有序的,所以只需要查找剩余位置,找到其中最小的数字5,然后和数组第二个位置的元素交换。
第三次选择,找到最小值7,和第三个位置的元素交换位置。
第四次选择,找到最小值8,和第四个位置的元素交换位置。
最后一个到达了数组末尾,没有可对比的元素,结束选择。
如此整个数组就排序完成了。
参考代码:
class Solution{
public:
void xz_sort(int* array, int length)
{
int min;
int index;
for (int i = 0; i < length; ++i) {
min = array[i];
index = i;
for (int j = i; j < length; ++j) {
if (min > array[j])
{
min = array[j];
index = j;
}
}
swap(array[i],array[index]);
}
}
};
图解插入
数组初始化:[8,2,5,9,7],我们把数组中的数据分成两个区域,已排序区域和未排序区域,初始化的时候所有的数据都处在未排序区域中,已排序区域是空。
第一轮,从未排序区域中随机拿出一个数字,既然是随机,那么我们就获取第一个,然后插入到已排序区域中,已排序区域是空,那么就不做比较,默认自身已经是有序的了。(当然了,第一轮在代码中是可以省略的,从下标为1的元素开始即可)
第二轮,继续从未排序区域中拿出一个数,插入到已排序区域中,这个时候要遍历已排序区域中的数字挨个做比较,比大比小取决于你是想升序排还是想倒序排,这里排升序:
第三轮,排5:
第四轮,排9:
第五轮,排7
排序结束。
参考代码:
class Solution{
public:
void insert_sort(int* array, int length)
{
if (length == 1)
return;
int current;
for (int i = 1; i < length; ++i) {
current = array[i];
for (int j = i-1; j >= 0 ; --j) {
if(current < array[j])
{
swap(array[j],array[j+1]);
} else{
break;
}
}
}
}
};
假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列。
3 1 2 5 4 6 9 7 10 8
在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。想一想,你有办法可以做到这点吗?
给你一个提示吧。请回忆一下冒泡排序,是如何通过“交换”,一步步让每个数归位的。此时你也可以通过“交换”的方法来达到目的。具体是如何一步步交换呢?怎样交换才既方便又节省时间呢?
方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即j=10),指向数字8。
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j--),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下。
6 1 2 5 9 3 4 7 10 8
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。
此时再次进行交换,交换之后的序列如下。
6 1 2 5 4 3 9 7 10 8
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下。
3 1 2 5 4 6 9 7 10 8
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。
左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧。
如果你模拟的没有错,调整完毕之后的序列的顺序应该是。
2 1 3 5 4
OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下。
1 2 3 4 5 6 9 7 10 8
对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下。
1 2 3 4 5 6 7 8 9 10
到此,排序完全结束。细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。
参考代码:
class Solution{
public:
//快速排序(分治思想)
void quick_sort(int* array, int start, int end)
{
if (start >= end)
return;
int base = array[start];
int i = start;
int j = end;
while (i < j)
{
while (i base)
{
swap(array[i],array[j]);
break;
} else{
i++;
}
}
if (i >= j)
{
swap(array[start],array[i]);
break;
}
}
quick_sort(array,start,i-1);
quick_sort(array,i+1,end);
}
};
使用三数中值分割法:一组序列的中值(中位数)是枢纽元最好的选择(因为可以将序列均分为两个子序列,归并排序告诉我们,这时候是O(NlogN);但要计算一组数组的中位数就比较耗时,会减慢快排的效率。但可以通过计算数组的第一个,中间位置,最后一个元素的中值来代替。比如序列:[8,1,4,9,6,3,5,2,7,0]。第一个元素是8,中间(left+right)/2(向下取整)元素为6,最后一个元素为0。所以中位数是6,即枢纽元是6。显然使用三数分割法消除了预排序输入的坏情形,并且实际减少了14%的比较。
void quick_sort(vector& arr, int start, int end)
{
if (start >= end)
return;
//采用三值法选base基准值
//
int pos = -1;
int mid = start + ((end - start) >> 1);
if (arr[start] >= arr[mid] && arr[mid] >= arr[end])
pos = start;
else if (arr[end] >= arr[mid] && arr[mid] >= arr[start])
pos = end;
else
pos = mid;
swap(arr[start], arr[pos]);
//
int base = arr[start];
int i = start;
int j = end;
while (i < j)
{
while (i < j)
{
if (arr[j] < base)
{
break;
}
--j;
}
while (i < j)
{
if (arr[i] > base)
{
swap(arr[i],arr[j]);
break;
}
++i;
}
}
if (i >= j)
{
swap(arr[i],arr[start]);
}
quick_sort(arr,start,i-1);
quick_sort(arr,i+1,end);
}
图解归并
我们以[8,2,5,9,7]这组数字来举例
首先,一刀切两半:
再切:
再切
粒度切到最小的时候,就开始归并
数据量设定的比较少,是为了方便图解,数据量为单数,是为了让你看到细节,下面我画了一张更直观的图可能你会更喜欢:
参考代码:
class Solution{
public:
int temp[100];//辅助数组
void merge_sort(int* array, int start, int end)
{
if (start >= end)
return;
int mid = start + (end - start)/2;
merge_sort(array,start,mid);
merge_sort(array,mid+1,end);
merge(array,start,end,mid);
}
void merge(int* array, int start, int end , int mid)
{
int left = start;
int right = mid + 1;
int k = start;
if (left >= right)
return;
while (left < mid + 1 && right < end + 1)
{
if (array[left] > array[right])
{
temp[k++] = array[right++];
} else{
temp[k++] = array[left++];
}
}
while (left < mid + 1)
{
temp[k++] = array[left++];
}
while (right < end + 1)
{
temp[k++] = array[right++];
}
for (int i = start; i < end + 1; ++i) {
array[i] = temp[i];
}
}
};