三种基础的排序算法及shell排序
在计算机科学所使用的排序算法通常被分类为:
计算的 时间复杂度(最差、平均、和最好性能),依据列表(list)的大小(n)。一般而言,好的性能是O(n log n),且坏的性能是O(n^2)。对于一个排序理想的性能是O(n)。仅使用一个抽象关键比较运算的排序算法总平均上总是至少需要O(n log n)。
存储器使用量(以及其他电脑资源的使用)
稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
依据排序的方法:插入、交换、选择、合并等等。
依据排序的方法分类的三种排序算法:
冒泡排序
冒泡排序对一个需要进行排序的数组进行以下操作:
比较第一项和第二项;
如果第一项应该排在第二项之后, 那么两者交换顺序;
比较第二项和第三项;
如果第二项应该排在第三项之后, 那么两者交换顺序;
以此类推直到完成排序;
实例说明:
代码实现java:
function swap(items, firstIndex, secondIndex){
var temp = items[firstIndex];
items[firstIndex] = items[secondIndex];
items[secondIndex] = temp;
};
function bubbleSort(items){
var len = items.length, i, j, stop;
for (i = 0; i < len; i++){
for (j = 0, stop = len-i; j < stop; j++){
if (items[j] > items[j+1]){
swap(items, j, j+1);
}
}
}
return items;
}
外层的循环决定需要进行多少次遍历, 内层的循环负责数组内各项的比较, 还通过外层循环的次数和数组长度决定何时停止比较.
冒泡排序极其低效, 因为处理数据的步骤太多, 对于数组中的每n项, 都需要n^2次操作来实现该算法(实际比n^2略小, 但可以忽略, 具体原因等待提莫前去探路), 即时间复杂度为O(n^2).
对于含有n个元素的数组, 需要进行(n-1)+(n-2)+…+1次操作, 而(n-1)+(n-2)+…+1 = n(n-1)/2 = n^2/2 - n/2, 如果n趋于无限大, 那么n/2的大小对于整个算式的结果影响可以忽略, 因此最终的时间复杂度用O(n^2)表示
shell
#!/bin/bash
vs() {
array[$1]=$[${array[$1]}^${array[$2]}]
array[$2]=$[${array[$1]}^${array[$2]}]
array[$1]=$[${array[$1]}^${array[$2]}]
}
read -p "请输入多个整数:" num
declare -a array
array=($num)
len=$[${#array[*]}-1]
for ((i=0; i < $len;i++));do
sto=$[$len-$i]
for ((j=0; j < $sto;j++));do
if [ ${array[$j]} -gt ${array[$[$j+1]]} ];then
vs $j $[$j+1]
fi
done
done
echo "${array[*]}"
选择排序
a) 原理:每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。也就是:每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。基于此思想的算法主要有简单选择排序、树型选择排序和堆排序。(这里只介绍常用的简单选择排序)
b) 简单选择排序的基本思想:给定数组:int[] arr={里面n个数据};第1趟排序,在待排序数据arr[1]~arr[n]中选出最小的数据,将它与arrr[1]交换;第2趟,在待排序数据arr[2]~arr[n]中选出最小的数据,将它与r[2]交换;以此类推,第i趟在待排序数据arr[i]~arr[n]中选出最小的数据,将它与r[i]交换,直到全部排序完成。
java代码实现
public class SelectionSort {
public static void main(String[] args) {
int[] arr={1,56,8,66,88,11};
System.out.println("交换之前:");
for(int num:arr){
System.out.print(num+" ");
}
//选择排序的优化
for(int i = 0; i < arr.length - 1; i++) {// 做第i趟排序
int k = i;
for(int j = k + 1; j < arr.length; j++){// 选最小的记录
if(arr[j] < arr[k]){
k = j; //记下目前找到的最小值所在的位置
}
}
//在内层循环结束,也就是找到本轮循环的最小的数以后,再进行交换
if(i != k){ //交换a[i]和a[k]
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
System.out.println();
System.out.println("交换后:");
for(int num:arr){
System.out.print(num+" ");
}
}
}
外层循环决定每次遍历的初始位置, 从数组的第一项开始直到最后一项. 内层循环决定哪一项元素被比较.
选择排序的时间复杂度为O(n^2).
shell
[root@cos7 app ]#cat selectSort.sh
vs() {
array[$1]=$[${array[$1]}^${array[$2]}]
array[$2]=$[${array[$1]}^${array[$2]}]
array[$1]=$[${array[$1]}^${array[$2]}]
}
read -p "请输入多个整数:" num
declare -a array
array=($num)
len=$[${#array[*]}-1]
leng=$[$len+1]
for ((i=0; i < $len;i++));do
min=$i
for ((j=$[$min+1]; j < $leng;j++));do
if [ ${array[$j]} -lt ${array[$min]} ];then
min=$j
fi
done
if [ $i -ne $min ];then
vs $i $min
fi
done
echo "${array[*]}"
插入排序
与上述两种排序算法不同, 插入排序是稳定排序算法(stable sort algorithm), 稳定排序算法指不改变列表中相同元素的位置, 冒泡排序和选择排序不是稳定排序算法, 因为排序过程中有可能会改变相同元素位置. 对简单的值(数字或字符串)排序时, 相同元素位置改变与否影响不是很大. 而当列表中的元素是对象, 根据对象的某个属性对列表进行排序时, 使用稳定排序算法就很有必要了.
一旦算法包含交换(swap)这个步骤, 就不可能是稳定的排序算法. 列表内元素不断交换, 无法保证先前的元素排列为止一直保持原样. 而插入排序的实现过程不包含交换, 而是提取某个元素将其插入数组中正确位置.
插入排序的实现是将一个数组分为两个部分, 一部分排序完成, 一部分未进行排序. 初始状态下整个数组属于未排序部分, 排序完成部分为空. 然后进行排序, 数组内的第一项被加入排序完成部分, 由于只有一项, 自然属于排序完成状态. 然后对未完成排序的余下部分的元素进行如下操作:
如果这一项的值应该在排序完成部分最后一项元素之后, 保留这一项在原有位置开始下一步;
如果这一项的值应该排在排序完成部分最后一项元素之前, 将这一项从未完成部分暂时移开, 将已完成部分的最后一项元素移后一个位置;
被暂时移开的元素与已完成部分倒数第二项元素进行比较;
如果被移除元素的值在最后一项与倒数第二项的值之间, 那么将其插入两者之间的位置, 否则继续与前面的元素比较, 将暂移出的元素放置已完成部分合适位置. 以此类推直到所有元素都被移至排序完成部分.
实例说明:
现在需要将数组var items = [5, 2, 6, 1, 3, 9];进行插入排序:
5属于已完成部分, 余下元素为未完成部分. 接下来提取出2, 因为5比2大, 于是5被移至靠右一个位置, 覆盖2, 占用2原本存在的位置. 这样本来存放5的位置(已完成部分的首个位置)就被空出, 而2在比5小, 因此将2置于这个位置, 此时结果为[2, 5, 6, 1, 3, 9];
接下来提取出6, 因为6比5大, 所以不操作提取出1, 1与已完成部分各个元素(2, 5, 6)进行比较, 应该在2之前, 因此2, 5, 6各向右移一位, 1置于已完成部分首位, 此时结果为[1, 2, 5, 6, 3, 9];
对余下未完成元素进行类似操作, 最后得出结果[1, 2, 3, 5, 6, 9];
java代码实现:
function insertionSort(items) {
let len = items.length, value, i, j;
for (i = 0; i < len; i++) {
value = items[i];
for (j = i-1; j > -1 && items[j] > value; j--) {
items[j+1] = items[j];
}
items[j+1] = value;
}
return items;
};
外层循环的遍历顺序是从数组的第一位到最后一位, 内层循环的遍历则是从后往前, 内层循环同时负责元素的移位.
插入排序的时间复杂度为O(n^2)
shell
[root@cos7 app ]#cat insertionSort.sh
read -p "请输入多个整数:" num
declare -a array
array=($num)
len=$[${#array[*]}-1]
leng=$[$len+1]
for ((i=0; i < $leng;i++));do
value=${array[$i]}
for (( j=$[$i-1];j > -1 && ${array[j]} > $value;j-- ));do
array[$[j+1]]=${array[j]}
done
array[$[j+1]]=$value
done
echo "${array[*]}"
以上三种排序算法都十分低效, 因此实际应用中不要使用这三种算法, 遇到需要排序的问题, 应该首先使用JavaScript内置的方法Array.prototype.sort();
shell希尔排序
public class ShellSort {
public static void main(String[] args) {
int[] array = { 49, 38, 65, 97, 76, 13, 27, 49, 55, 4 };
shellSort(array);
for (int i = 0; i < array.length; i++)
System.out.print(array[i] + " ");
}
public static void shellSort(int[] arr) {
int length = arr.length;
int d = length;
int times = 0;
while (true) {
d = d / 2;
for (int i = 0; i < d; i++)
for (int j = i; j < length; j = j + d) {
int k;
int temp = arr[j];
for (k = j - d; k >= 0 && temp < arr[k]; k = k - d) {
arr[k + d] = arr[k];
}
arr[k+d] = temp;
}
times++;
System.out.println("第" + times + "趟排序结果:");
for (int i = 0; i < arr.length; i++)
System.out.print(arr[i] + " ");
System.out.println();
if (d == 1)
break;
}
}
}
shell
[root@cos7 app ]#bash shellSort.sh
89 12 65 97 61 81 27 2 61 98
2 12 27 61 61 65 81 89 97 98
[root@cos7 app ]#cat shellSort.sh
#!/bin/bash
ShellSort() {
#希尔排序 时间复杂度不确定
#gap 差距 的意思
#增量的选择
for(( gap=$[${#array[*]}/2];gap > 0;gap /= 2 ));do
#n-gap 趟排序
for(( i=$gap;i < ${#array[*]};i++ ));do
tmp=${array[i]};
for(( j=$i;j >= $gap && $tmp < ${array[$[j-$gap]]};j-=$gap ));do
array[j]=${array[$[j-$gap]]}
done
array[j]=$tmp
done
done
}
main() {
declare -a array
array=("89" "12" "65" "97" "61" "81" "27" "2" "61" "98")
echo "${array[*]}"
ShellSort $array
echo "${array[*]}"
}
main
汉诺塔
汉诺塔问题源于印度神话
那么好多人会问64个圆盘移动到底会花多少时间?那么古代印度距离现在已经很远,这64个圆盘还没移动完么?我们来通过计算来看看要完成这个任务到底要多少时间?
我们首先利用数学上的数列知识来看看F(n=1)=1,F(n=2)=3,F(n=3)=7,F(n=4)=15……F(n)=2F(n-1)+1;
我们使用数学归纳法可以得出通项式:F(n)=2^n-1。当n为64时F(n=64)=18446744073709551615。
我们假设移动一次圆盘为一秒,那么一年为31536000秒。那么18446744073709551615/31536000约等于584942417355天,换算成年为5845.54亿年。
目前太阳寿命约为50亿年,太阳的完整寿命大约100亿年。所以我们整个人类文明都等不到移动完整圆盘的那一天。
有很多人对汉诺塔的解法产生了兴趣。从一阶汉诺塔到N阶汉诺塔它们是否有规律性的算法?
我们在使用程序实现它之前我们来分析分析汉诺塔的解法:
我们设定三个柱子A,B,C。我们的目的是将环从A–>C。
public class Hanoilmpl {
public void hanoi(int n, char A, char B, char C) {
if (n == 1) {
move(A, C);
} else {
hanoi(n - 1, A, C, B);//步骤1 按ACB数序执行N-1的汉诺塔移动
move(A, C); //步骤2 执行最大盘子移动
hanoi(n - 1, B, A, C);//步骤3 按BAC数序执行N-1的汉诺塔移动
}
}
private void move(char A, char C) {//执行最大盘子的从A-C的移动
System.out.println("move:" + A + "--->" + C);
}
public static void main(String[] args) {
Hanoilmpl hanoi = new Hanoilmpl();
System.out.println("移动汉诺塔的步骤:");
hanoi.hanoi(3, 'a', 'b', 'c');
}
}
shell
echo a
echo b
echo c
给一个函数,只允许输入整数1到无穷大,用变量num表示
#无论多少盘片,都是一步一步在A B C 3个圆柱间移动
move1() {
echo "从$1到$2"
}
move2() {
if [ $1 -eq 1 ];then
move1 $2 $4
else
move2 $[$1-1] $2 $4 $3
move1 $2 $4
move2 $[$1-1] $3 $2 $4
fi
}
move2 $1 A B C
[root@cos7 app ]#bash hannuota.sh 3
从A到C
从A到B
从C到B
从A到C
从B到A
从B到C
从A到C