排序
一.插入排序
1.排序原理
将新的元素重复的插入到已经排好序的子线性表中,直到线性表全部排好序
2.具体步骤
将list[i]插入到已经排好序的list[0]~list[i-1]中,首先需要设置一个临时变量currentElement,然后从i-1向前进行比较,如果list[i-1]>currentElement,那么list[i]=list[i-1];如果list[i-2]>current,那么list[i-1]=list[i-2],依次类推,直到list[k]<=currentElement或者k<0停止,最后再将currentElement赋值给list[k+1]。
3.图解
例如排序 2,9,5,4,8,1,6
4.代码实现
实现方法可能有很多种,这里就列出一种(^ v ^)
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] a = { 2, 9, 5, 4, 8, 1, 6 };
for (int i = 1; i < a.length; i++) {
int current = a[i];//设置临时变量为a[i]
int k;
for (k = i - 1; k >= 0 && a[k] > current; k--) {//从i-1开始往前比较
a[k + 1] = a[k];
}
a[k + 1] = current;//比较完成后将临时变量插入到a[k+1]的位置
}
for (int i : a) {
System.out.print(i + " ");//1 2 4 5 6 8 9
}
}
5.时间复杂度
这里给出的插入排序是重复的将一个新元素插入到一个已经排好序的部分数组中,在进行第k次迭代时,要将元素插入到长度为k的数组中,需要进行k次比较,以及k次的移动插入元素,设T(n)为插入排序的时间复杂度,c表示诸如迭代中的赋值和额外的比较的操作总数
T(n)=(2+c)+(2x2+c)+(2x3+c)+……+(2x(n-1)+c)
=2(1+2+3+……+n-1)+(n-1)*c
=n(n-1)+cn-c
=O(n^2)
二.冒泡排序
1.排序原理
多次遍历数组,每次遍历连续比较相邻元素,如果没有按照规定顺序排序,交换位置
2.具体步骤
遍历数组并通过比较和交换将最大的值放到数组的最后一个位置,每一次遍历都可以比较出最大的数,那么比较list.length-1次就可以排好序了
3.图解
例如排序 2,9,5,4,8,1,6
4.代码实现
注意
- 如果在第k次遍历没有进行交换,那么证明数组已经排好序了,不用再进行第k+1及以后的比较
- 最后 i 位数肯定是最大的,所以不用与最后 i 位数比较
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] a = { 2, 9, 5, 4, 8, 1, 6 };
boolean b = true;//判断是否进行了位置交换,初始化为需要
for (int i = 1; i < a.length && b; i++) {//只有上一次进行了位置交换才进行下一次遍历
b = false;//开关
for (int j = 0; j < a.length -1 - i; j++) {//比较a.length-i之前的数组,因为最后的i位都是已经排好序的
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
b = true;//表明进行了位置交换
}
}
}
for (int i : a) {
System.out.print(i + " ");
}
}
5.时间复杂度
最好的情况下需要比较n-1次,时间复杂度为O(n)
最坏的情况下:
第一次循环需要比较n-1次
第二次 n-2次
第n-1次 1次
T(n)=(n-1+n-2+n-3+……+1)
=n*(n-1)/2
=O(n^2)
三.归并排序
1.排序原理
讲一个数组分为两部分,并且递归的调用归并排序,直到两部分都已经排好序了,再将他们合并到一起
2.具体步骤
先讲一个数组分为两部分,list(1)和list(2),再将list(1)分为list(1)(1)和list(1)(2)直到list(1)(1)不能分了为止,然后排序好list(1)(1),再排序好list(2)(2),然后再归并回list(1),list(2)用此相同操作,最后再将list(1)和list(2)归并得到排好序的list。
3.图解
4.代码实现
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] list = { 2, 9, 5, 4, 8, 1, 7, 6 };
devide(list);// 归并排序
for (int i : list) {// 循环输出
System.out.print(i + " ");
}
}
public static void devide(int[] list) {
if (list.length > 1) {
int[] left = new int[list.length / 2];
System.arraycopy(list, 0, left, 0, list.length / 2);
devide(left);// 递归直到数组就剩一个元素,肯定是有序的
int reaminLength = list.length - list.length / 2;
int[] right = new int[reaminLength];
System.arraycopy(list, list.length / 2, right, 0, reaminLength);
devide(right);// 递归另一部分,直到数组只剩一个元素
sort(left, right, list);// 排序并合并刚才的两部分数组
}
}
public static void sort(int[] left, int[] right, int[] list) {
int currentLeft = 0;// 左边数组索引
int currentRight = 0;// 右边数组索引
int currentList = 0;// 临时数组索引
while (currentLeft < left.length && currentRight < right.length) {// 如果两个数组中元素都没有耗尽
if (left[currentLeft] < right[currentRight]) {// 从左边数组第一个开始和右边数组第一个进行比较
list[currentList++] = left[currentLeft++];// 如果左边的小于右边的,则将左边数组的这个元素放入到临时数组中,索引同时向后移
} else {
list[currentList++] = right[currentRight++];// 同上
}
}
while (currentLeft < left.length) {// 这里比较的是如果上方while循环提前将right耗尽了,则将left中剩余元素加到list后面
list[currentList++] = left[currentLeft++];
}
while (currentRight < right.length) {// 同上
list[currentList++] = right[currentRight++];
}
}
5.时间复杂度
排序两端的数组需要的时间复杂度是T(n/2)+T(n/2),要归并两个子数组,需要进行n-1次的排序,然后在进行n次将元素放入到临时数组中
T(n)=T(n/2)+T(n/2)+(n-1)+n
=O(n*logn)
四.快速排序
1.排序原理
设置一个主元素,将数组分为两部分,进行递归快速排序,使得第一部分的数全部小于主元素,第二部分的数全部大于主元素,直到数组全部排好
2.具体步骤
设置第一个数为主元素,设置一个左指针和一个右指针,如果右指针所对应的元素小于主元素,那么就将此元素放入到左指针所对应的元素上,左指针向后移,此时开始从左指针向右移动,像这样循环往复,最后将主元素插入到指针结束移动的位置
3.图解:
这是引用《java语言程序设计——进阶篇》中的一幅图:
4.代码
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] a = { 2, 9, 5, 4, 8, 1, 6, 7 };
quickSort(a, 0, a.length - 1);
for (int i : a) {
System.out.print(i + " ");
}
}
public static void quickSort(int[] a, int low, int high) {
if (low > high) {
return;
}
int leftIndex = low;// 设置左边索引从low开始向右移动
int rightIndex = high;// 设置右边索引从high开始向左移动
boolean b = true;// 设置默认从右边开始比较
int mainElement = a[low];// 设置主元素:该数组中的第一个元素,无论是大数组还是小数组
L: while (leftIndex < rightIndex) {// 在左边索引小于右边索引的时候进行循环
if (b) {// 判断索引从右边开始向左移动
for (int i = rightIndex; i > leftIndex; i--) {// 索引从右边开始向左移动
if (a[i] < mainElement) {// 如果i位置元素<主元素
a[leftIndex++] = a[i];// 左边索引所对应的位置赋为a[i]
b = false;// 改为从左边向右比较
continue L;// 直接进行while循环,就不会执行下面的右边索引等于左边索引
} else {
rightIndex--;// 如果大于则将右边索引向左移动
}
}
rightIndex = leftIndex;// 全部遍历之后没有一个元素比主元素小,那么说明排序是正确的,就将右索引值赋给左索引
} else {// 判断索引从左边开始向右移动 下方代码与上方类似,只不过方向相反
for (int j = leftIndex; j < rightIndex; j++) {
if (a[j] > mainElement) {
a[rightIndex--] = a[j];
b = true;
continue L;
} else {
leftIndex++;
}
}
leftIndex = rightIndex;
}
}
a[leftIndex] = mainElement;// 全部排好后,将主元素插入到索引leftIndex或者rightIndex中,因为两个索引相同
quickSort(a, low, leftIndex - 1);// 递归排序左索引之前的子数组
quickSort(a, leftIndex + 1, high);// 递归排序左索引之后的子数组
}
5.时间复杂度
在最差的情况下,划分由n个元素组成的数组需要进行n次比较和n次移动,因此划分时间需要O(n)。
在最差的情况下,每次主元素会将数组划分为一个大数组和一个空数组,这个大的子数组在规模上会比上一次划分少1,该算法需要(n-1)+(n-2)+……+1=O(n^2)
在最好的情况下,每次主元素将数组划分为规模差不多相等的子数组
T(n)=T(n/2)+=T(n/2)+n=O(n*logn)
五.堆排序
1.排序原理
创建一个Heap对象,使用add方法将元素添加到堆中,然后使用remove方法降序删除元素
2.具体步骤
首先创建一个Heap类,然后创建Heap对象,将元素添加到堆中,再降序删除元素
3.图解
这里的堆所指的是二叉堆,二叉堆具备以下两个条件:
- 形状属性:它必须是一颗完全二叉树
- 堆属性:每个节点大于等于他的任意一个孩子
而完全二叉树的概念是:一棵二叉树的每一层都是满的,或者最后一层可以不填满并且最后一层的叶子都是向左放置的
下面这个图就是一个二叉堆:
注意
在数组中存储为[39,32,33,16,24,27,30]
不难发现,
左孩子的索引是父索引乘2+1;
右孩子的索引是父索引乘2+2;
下面这个图虽然是二叉树,但是不是一个二叉堆:
4.代码实现
- 首先创建一个堆类
public class Heap<E extends Comparable<E>> {
private ArrayList<E> list = new ArrayList<>();
public Heap() {
}
public Heap(E newObject) {
add(newObject);// 执行add方法
}
public void add(E newObject) {
list.add(newObject);// 将对象加入到list中
int currentIndex = list.size() - 1;// 设置当前索引为最后一个元素的下标,即刚才插入的元素的下标
while (currentIndex > 0) {// 只要当前索引值值大于0,就继续循环
int parentIndex = (currentIndex - 1) / 2;// 获得父节点索引
if (list.get(currentIndex).compareTo(list.get(parentIndex)) > 0) {// 如果子孩子的值大于父节点的值
E tempObject = list.get(currentIndex);// 交换孩子节点和父节点的位置
list.set(currentIndex, list.get(parentIndex));
list.set(parentIndex, tempObject);
} else
break;// 跳出循环
currentIndex = parentIndex;// 将父节点索引赋给孩子节点
}
}
public E remove() {
if (list.size() == 0) {// 如果数组列表长度为零,返回null
return null;
}
E removeElement=list.get(0);//删除最大的元素,并最终返回,接下来的步骤是将此二叉树重新整理为二叉堆
list.set(0, list.get(list.size()-1));//将最后一个元素放到根节点的位置
list.remove(list.size()-1);//移除最后一个节点
int currentIndex=0;//从第一个元素开始向下
while(currentIndex<list.size()) {//如果当前索引<数组列表的长度
int leftIndex=currentIndex*2+1;//左孩子索引
int rightIndex=currentIndex*2+2;//右孩子索引
if(leftIndex>=list.size()) {break;}//如果左孩子索引大于列表长度
int maxIndex=leftIndex;//设置最大索引为左孩子索引
if(rightIndex<list.size()) {
if(list.get(maxIndex).compareTo(list.get(rightIndex))<0) {//如果左孩子索引大于右孩子索引
maxIndex=rightIndex;//右索引赋给最大索引
}
}
if(list.get(currentIndex).compareTo(list.get(maxIndex))<0) {//如果当前元素小于最大值索引
E tempObject=list.get(currentIndex);//交换元素
list.set(currentIndex, list.get(maxIndex));
list.set(maxIndex, tempObject);
currentIndex=maxIndex;//最大索引赋给当前索引
}
else break;
}
return removeElement;
}
public int getSize() {//返回heap大小
return list.size();
}
}
- 测试堆排序
public static void main(String[] args) {
// TODO Auto-generated method stub
Integer[] list= {-44,-5,-3,3,3,1,-4,0,1,2,4,5,53};
heapSort(list);//堆排序
for(int i:list) {//打印
System.out.print(i+" ");
}
}
public static <E extends Comparable<E>> void heapSort(E[] list) {
Heap<E> heap=new Heap<>();
for(int i=0;i<list.length;i++) {
heap.add(list[i]);//将列表中元素添加到堆中
}
for(int i=list.length-1;i>=0;i--) {
list[i]=heap.remove();//将堆中元素取出放入list中
}
}
5.时间复杂度
由于堆是一个完全二叉树所以第一层有一个节点,第二层有两个节点,第n层有2^(n-1)个节点
1+2+……+2^(n-2)<h
h<1+2+……+2^(n-1)
所以堆的高度为O(logn)
向堆中添加一个元素最多需要n步,有n个元素的数组的初始堆需要O(nlogn)时间
各个排序的时间复杂度: