一、排序
排序是算法学习过程中入门必学的模块之一,虽然在实际编程时很少有程序员动手写排序算法(C++库中封装的sort函数是底层是快排实现的)。下面我将介绍最常见的三种排序算法:(1)冒泡排序 (2)选择排序 (3)快速排序,其中冒泡排序和选择排序
1. 蛮力法
(1)冒泡排序
冒泡排序,顾名思义,排序过程就像冒泡一样,每次将最大的数冒到最上面并固定,然后依次将剩余的元素的最大值再冒到最上面,如下图所示:
算法:BubbleSort(A[0...n-1])
// 该算法用冒泡排序对数组A[0...n-1]排序
// 输入:一个可排序的数组A
// 输出:非降序排列的数组
for i ⬅ 0 to n-2 do
for j ⬅ 0 to n-2-i do
if A[j + 1] < A[j]
swap A[j] and A[j + 1]
例如下面的例子,第一次循环时,我们目标是将整个数组的最大值置于队尾
下面来解读一下上面的伪代码:一共有两重循环,第一重循环是冒泡的次数,要想对整个数组进行排序,只需要对n - 1个数字进行排序(即找出前n - 1大的数,剩下一个位置和一个数字,不需要排序),而每次排序的目的是将待排序数字中的最大值冒到数组最末尾,第一次循环(i=0)只需要 j 从0到n-2(倒数第二个位置), 分别比较A[j]和A[j+1],如果前者更大,则交换二者,最终将最大值冒到最后。第二次循环(i=1),则 j 从0到n - 3 ······ 因此,第二重循环为j从0 ~ j - 2 - i。因为冒泡排序是两重规模为n的循环,因此时间复杂度为O(n^2)
(2)选择排序
选择排序和冒泡排序的思想很相似,也是一次循环处理将一个元素归位,每次将待排序数组中最小值置于首位。和冒泡排序的区别是:冒泡排序是不停的进行两两交换,而选择排序每次循环中不交换,而是使用一个变量来记录目前遍历到的元素的最小值的位置,当遍历完整个数组后一次性进行交换。伪代码如下:
算法:SelectionSort(A[0...n-1])
// 该算法用选择排序对给定的数组排序
// 输入:一个可排序数组A[0...n-1]
// 输出:非降序排列的数组A[0...n-1]
for i ⬅ 0 to n-2 do
min ⬅ i
for j ⬅ i+1 to n-1 do
if A[j] < A[min]
min ⬅ j
swap A[i] and A[min]
当然,选择排序的核心还是二重循环,因此时间复杂度仍然是O(n^2)
2. 分治算法(快速排序)
快速排序算法是目前公认的平均效率最快的算法,和其他算法相比,快排的特点是比较次数和交换次数都很少,平均时间复杂度只有O(nlogn),下面一起来看一下是如何实现的吧:
(1)选定一个数字作为基准值pivotKey,这里方便起见,规定为最右侧元素6
(2)两个标记符(指针)low,high分别指向首元素A[0]和倒数第二个元素A[n-2]
(3)将low右移,只要满足A[low] < pivotKey ,就继续右移,直到不满足时,停止右移,考虑high指针;同理,将high左移,只要满足A[high] > pivotKey,就继续左移,直到不满足。
(4)直到两边都不满足时,交换二者。(左标记low是为了找到大于pivotKey的数字,右标记是为了找到小于pivotKey的数字,通过交换,可以在左侧收集到小于pivotKey的数组,右侧收集到大于pivotKey的数字)
(5)只要low < high,就不断执行上面步骤,当low >= high时,其实跳出循环时low == high,此时指针所指向的位置就是pivotKey元素排序后应该在的位置k
(6)分别对k左边的子数组和右边的子数组执行上述步骤,最终得到排序好的数组。
int Partition(SqList &L, int low, int high) {
KeyType pivotkey;
pivotkey = L.r[low].key;
while (low<high) {
while ((low<high)&&(L.r[high].key>=pivotkey))
--high;
L.r[low] ←→ L.r[high];
while ((low<high)&&(L.r[low].key<=pivotkey))
++low;
L.r[low] ←→ L.r[high];
}
return low; // 返回枢轴位置
} // Partition
QUICKSORT(s,t,A)
LIST F;`
int s,t;
{
int i;
if (s<t) {
i=PARTITION(s,t,A);
QUICKSORT(s,i-1,A);
QUICKSORT(i+1,t,A);
}
}
二、折半查找
折半查找的核心在于折半,也就是只需要进行一次比较就可以删除一半的元素。要想实现这样的效果,就必须保证数组是有序的,只需要比较待查找元素和中间位置元素的大小关系即可。如果待查找元素比中间元素小,则该元素肯定在左边一半中出现(或者不出现),同理,如果待查找元素比中间元素大,则该元素肯定在右边一半中出现(或者不出现)。
算法 BinarySearch(A[0..n-1],K)
//非递归折半查找
//输入:升序数组A[0..n-1]和查找键K
//输出:找到键K,返回K所在下标,否
则返回-1
l ⬅ 0; r ⬅ n-1;
while (l ≤ r) do{
m ⬅ (l + r) / 2;
if (K = A[m]) return m;
else if (K<A[m]) r ⬅ m-1;
else l ⬅ m+1;
}
return -1;