1.冒泡排序:
原理:比较两个相邻的元素,将值大的元素交换至右端。重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
时间复杂度:最好的情况:数据正序,只需要走一趟即可完成排序,时间复杂度O(n);
最坏的情况:数据反序,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置,则比较的次数为:n(n-1)/2,移动的次数为:3n(n-1)/2;时间复杂度为O(n²);
综上冒泡排序的平均时间复杂度为O(n²);
冒泡排序java实现:
public int[] bubbleSort(int[] arr){
int temp=0;
for(int i=0;i<arr.length-1;i++){
for(int x=0;x<arr.length-1-i;x++){
if(arr[x]>arr[x+1]){
temp=arr[x];
arr[x]=arr[x+1];
arr[x+1]=temp;
}
}
}
return arr;
}
2.直接插入排序:
原理:直接插入排序的基本操作是将一个记录插入到有序表中,从而得到一个新的、记录数增1的有序表。对于给定的一组记录,初始时假定第一个记录自成一个有序序列,其余记录为无序序列。接着从第二个记录开始,按照记录的大小依次将当前处理的记录插入到其之前的有序序列中,直到最后一个记录插到有序序列中为止。
时间复杂度:最好的情况:也就是要排序的表本身就是有序的,此时只有数据比较,没有数据移动,时间复杂度为O(n)。
最坏的情况:即待排序的表是逆序的情况,此时需要比较次数为:1+2+3+…+(n-1)=(n)(n-1)/2 次,而记录移动的最大值也达到了 (n+1)n/2 次; 时间复杂度为O(n²)。
综上直接插入排序的时间复杂度为O(n²)。
直接插入排序的java实现:
public int[] insertSort(int arr[]){
for(int i =1;i<arr.length;i++){
int temp=arr[i];
for(int x=i;x>0;x--){
if(temp<arr[x-1]){
arr[x]=arr[x-1];
if(x==1){
arr[x-1]=temp;
break;
}
}else{
arr[x]=temp;
break;
}
}
}
return arr;
}
3.希尔排序:
原理:想弄清楚希尔排序首先得弄明白快速插入排序算法。希尔排序也成为“缩小增量排序”,其基本原理是,现将待排序的数组元素分成多个子序列,使得每个子序列的元素个数相对较少,然后对各个子序列分别进行直接插入排序,待整个待排序列“基本有序”后,最后在对所有元素进行一次直接插入排序。因此,我们要采用跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。希尔排序是对直接插入排序算法的优化和升级。
概括说来:在“增量”为1之前,算法所做的工作就是为了让数组先基本有序。所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间。
时间复杂度:希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式移动,使得排序的效率提高。需要注意的是,增量序列的最后一个增量值必须等于1才行。另外,由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。
希尔排序最好时间复杂度和平均时间复杂度都是:O(nlog2n),最坏时间复杂度为:O(n²)。
希尔排序的java实现:
public int[] shellSort(int arr[]){
for(int increment =arr.length/2;increment>0;increment/=2){
for(int i=increment;i<arr.length;i++){
int temp=arr[i];
for(int x=i;x>=increment;x-=increment){
if(temp<arr[x-increment]){
arr[x]=arr[x-increment];
if(x<2*increment){
arr[x-increment]=temp;
break;
}
}
else{
arr[x]=temp;
break;
}
}
}
}
return arr;
}
4.简单选择排序:
原理:给定数组:int[] arr={里面n个数据};第1趟排序,在待排序数据arr[1]~arr[n-1]中选出最小的数据temp,和其对应的下标position,将它与arr[0]比较,temp小交换就交换arr[0]和arr[position]的位置;第2趟,在待排序数据arr[2]~arr[n-1]中选出最小的数据temp,和其对应的下标position,将它与arr[1]比较,temp小交换就交换arr[1]和arr[position]的位置;以此类推,第i趟在待排序数据arr[i]~arr[n-1]中选出最小的数据temp,将它与arr[i-1]做相似的比较交换,直到全部排序完成。
时间复杂度:选择排序的时间复杂度:简单选择排序的比较次数与序列的初始排序无关。 假设待排序的序列有 n个元素,则比较次数永远都是n(n - 1) / 2。而移动次数与序列的初始排序有关。当序列正序时,移动次数最少,为 0。当序列反序时,移动次数最多,为3n (n - 1) / 2。所以,综上,简单排序的时间复杂度为 O(n²)。
简单选择排序的java实现:
public int[] selectSort(int arr[]){
for(int i=1;i<arr.length;i++){
int temp=arr[i];
int position=i;
for(int x=i;x<arr.length-1;x++){
if(temp>arr[x+1]){
temp=arr[x+1];
position=x+1;
}
}
if(arr[i-1]>temp){
arr[position]=arr[i-1];
arr[i-1]=temp;
}
}
return arr;
}
5.堆排序:
堆是一棵顺序存储的完全二叉树。
其中每个结点的关键字都不大于其子结点的关键字,这样的堆称为小根堆。
其中每个结点的关键字都不小于其子结点的关键字,这样的堆称为大根堆。
设最大堆的当前元素在数组中以R[i]表示,那么,
(1) 它的左孩子结点是:R[2*i+1];
(2) 它的右孩子结点是:R[2*i+2];
(3) 它的父结点是:R[(i-1)/2];
(4) R[i] => R[2*i+1] 且 R[i] => R[2i+2] 且 R[i]<=R[(i-1)/2]。先弄清楚大根堆的这4点基本属性。
堆排序的基本思想是:将待排序序列构造成一个大根堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个大根堆,这样会得到这n-1个元素的最大值(即为整个序列的第二大值)。如此反复执行,便能得到一个有序序列了。
弄清楚大根堆的数据结构和堆排序的实现原理,结合具体java实现代码来学习堆排序。
时间复杂度:堆排序是一种不稳定的排序方法。排序的稳定性是指如果在排序的序列中,存在前后相同的两个元素的话,排序前 和排序后他们的相对位置不发生变化。因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况。
堆排序过程的时间复杂度是O(nlgn)。因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是O(nlgn)
堆排序的java实现:
public int[] heapSort(int arr[]){
for(int i=(arr.length-1)/2;i>=0;i--){//从最后一个有子节点的根节点开始遍历调整,使整个数列成为大根堆
adjustHeap(arr,i,arr.length);
}
int temp=0;
for(int i=arr.length-1;i>=0;i--){
temp=arr[i];
arr[i]=arr[0];
arr[0]=temp;//交换首尾节点的位置,即把最大值调整到数组的末尾位置
adjustHeap(arr,0,i);
}
return arr;
}
//调整为大根堆
public void adjustHeap(int arr[],int parent,int length){
int temp=arr[parent];
for(int child=2*parent+1;child<length;child=2*child+1){
if(child+1<length&&arr[child]<arr[child+1]){//获取左右子节点中较大的子节点
child++;
}
if(temp<arr[child]){//①注意这里是较大的子节点和temp比较
arr[parent]=arr[child];
parent=child;
}else{
break;
}
}
arr[parent]=temp;//和①联立起来思考比较容易理解,也可以在①那里直接交换parent和child但是数据量大的话会影响效率
}
6.快速排序:
原理:选取基准点(理论上可以选取数列中任意数字为基准点),把数列分成已知的三部分,左部分都比基准点小,右部分都比基准点大,中间部分为尚未与基准点做比较的位置数据,在分别迭代快排左部和右部的数列,直至左部,右部完全有序;再对整个数列执行基准点比较分区,对分区后的左部和右部迭代至完全有序;重复上述操作直至整个序列被分成有序的左、右两个部分(左部都不大于基准点右部都不小于基准点,中间部分的数据为空,左右皆已有序),此时整个数据已完全有序。概括起来:选取基准点对数列进行分区,再对分区后的数据分别排序,直至整个数列有序。(具体实现请参悟代码)
时间复杂度:当基数值不能很好地分割数组,即基准值将数组分成一个子数组中有一个记录,而另一个子组组有 n -1 个记录时,下一次的子数组只比原来数组小 1,这是快速排序的最差的情况。如果这种情况发生在每次划分过程中,那么快速排序就退化成了冒泡排序,其时间复杂度为O(n²)。如果基准值都能讲数组分成相等的两部分,则出现快速排序的最佳情况。在这种情况下,我们还要对每个大小约为 n/2 的两个子数组进行排序。在一个大小为 n 的记录中确定一个记录的位置所需要的时间为O(n)。快速排序的平均时间复杂性为O(nlogn)。
快速排序的java实现:
public int[] quickSort(int arr[],int low,int high){
int start=low;
int end=high;
int key =arr[start]; //这里是以当前数列的第一个数据为基准点
while(low<high){
while(low<high&&arr[high]>=key){
high--;
}
//交换数列右边基准点比基准点小的值和基准点的位置
arr[low]=arr[high];
arr[high]=key;
while(low<high&&arr[low]<=key){
low++;
}
//交换数列左边基准点比基准点大的值和基准点的位置
arr[high]=arr[low];
arr[low]=key;
if(low>start)quickSort(arr,start,low-1);//递归,对在start和low-1之间的数据进行排序直至有序
if(end>high)quickSort(arr,high+1,end);//递归,对在high+1和end之间的数据进行排序直至有序
}
return arr;
}
7.归并排序:
原理:归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并过程为:比较a[i]和b[j]的大小,若a[i]≤b[j],则将第一个有序表中的元素a[i]复制到临时表r[k]中,并令i和k分别加上1;否则将第二个有序表中的元素b[j]复制到r[k]中,并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元。归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。归并排序的过程如下图所示。
临时表的创建和插入元素的过程:①:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;②:设定两个指针,最初位置分别为两个已经排序序列的起始位置;③:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;④:重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾。
时间复杂度:归并排序比较占用内存,但却是一种效率高且稳定的算法。 该算法中最好、最坏和平均的时间复杂度为O(nlog2n)。
归并排序算法的java实现:
public static int[] mergesSort(int[] nums, int low, int high) {
int mid = (low + high) / 2; //划分子序列的中间点
if (low < high) {
// 左边
mergesSort(nums, low, mid);
// 右边
mergesSort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
return nums;
}
//归并
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1]; //新建一个临时数组,用来存储当前排序后的数据
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中(左右两边已是有序数列)
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入临时数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边剩余的数移入临时数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
8.基数排序
基数排序是这样一种排序算法,我们可以从低位(个位)开始,根据个位数排序一次,然后根据十位数排序,再根据百位数进行排序……最终完成整个数组的排序。 对于十进制数字而言,每一位只会是 0~9 这十个数字,我们通常使用桶排序(计数排序)来完成每一位数的排序。桶排序是一种稳定的排序算法,基数排序的正确性依赖一种稳定的排序算法。 * 基数排序其实是分 LSD(从低位向高位排序) 和 MSD(从高位向低位排序) 两种。 基数排序的时间复杂度为 O(n)。 基数排序使用桶排序对其每一位进行排序,即每一位的排序时间复杂度为 O(n),假设最大的数有 digit 位,则共需要进行 digit * O(n) 次排序。时间复杂度依旧为 O(n)。
public void radixSort(int[] arr) {
int digit = getMaxDigit(arr);
for (int i = 1; i <= digit; i++) {
bucketSort(arr, i);
}
}
//获取数组中数据的最长位数
public int getMaxDigit(int[] arr) {
int digit = 1;
int base = 10;
for (int i : arr) {
while (i > base) {
base *= 10;
digit++;
}
}
return digit;
}
//按当前位的大小(digit)进行桶排序
public void bucketSort(int[] arr, int digit) {
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
bucketList.add(new ArrayList<>());
}
int base = (int) Math.pow(10, digit - 1);
for (int i : arr) {
int index = i / base % 10;
bucketList.get(index).add(i);
}
int x = 0;
for (ArrayList<Integer> bucket : bucketList) {
for (int i : bucket) {
arr[x++] = i;
}
}
}