一、简单排序
program 1
关于接口对象 ,代表什么意思,接口应该不能创建实例的,但是为啥存在接口的实例,关于Comparable接口,Student类继承Comparable接口,创建get Max方法创建新的命令,输入输出采用的就是Comparable对象了。
package test;
import sortAlgorithm.Student;
public class test{
public static Comparable getMax(Comparable c1,Comparable c2){
return (c1.compareTo(c2)>=0)?c1:c2;
}
public static void main(String[] args){
Student s1 = new Student();
s1.setAge(12);
s1.setName("张三");
Student s2 = new Student();
s2.setName("李四");
s2.setAge(15);
System.out.println(getMax(s1,s2));
long startTimes = System.currentTimeMillis();
long endTimes = System.currentTimeMillis();
System.out.println("运行时间为"+(endTimes-startTimes));
}
}
package sortAlgorithm;
public class Student implements Comparable<Student>{
String name;
int age;
public int getAge() {
return age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", studentNum=" + age +
'}';
}
@Override
public int compareTo(Student o) {
return this.getAge()-o.getAge();
}
}
answer One
接口与抽象类类似,表示一个大类,对外部来讲,相当于一个黑匣子,当接口的实例进行不同的操作时,你会发现接口的方法会根据不同的子类型,进行不同的操作,也就是接口是一个黑匣子的外部接口。
interface Person{
void eat();
void sleep();
}
class Studentimplements Person{
public void eat(){
System.out.println("学生去食堂吃饭!");
}
public void sleep(){
System.out.println("学生回寝室睡觉!");
}
}
class Teacherimplements Person{
public void eat(){
System.out.println("教师去教工餐厅吃饭!");
}
public void sleep(){
System.out.println("教师回学校公寓睡觉!");
}
}
class Parents implements Person{
publicvoid eat(){
System.out.println("家长去招待所饭馆吃饭!");
}
public void sleep(){
System.out.println("家长回招待所睡觉!");
}
}
public class PersonInterface{
public static void main(String[] args)
{
Person p=new Student(); //创建一个Person接口,没有实例,实例是接口的继承了接口的对象,上面创建了一个学生实例,完成了学生的操作,下面创建了一个老师实例和父母实例。分别按照老师和父母的行为完成了他们相应的操作。
p.eat();
p.sleep();
p=new Teacher();
p.eat();
p.sleep();
p=new Parents();
p.eat();
p.sleep();
}
}
program 2
接口 和继承还存在很大的问题,原因缺少实战,以后练完数据结构再练实战项目,先拿到offer,35万年薪offer,五年内年薪破百万。
排序方法中一定要采用交换数据,不能采用直接赋值,直接赋值会对某些元素重复使用。
1.冒泡排序法
简介,比较相邻两个元素值,大的放在后面,小的放在前面,第一遍会直接将最大的值放到最后面,然后不看这个元素,然后再对剩下的元素进行排列,第二遍会将第二大的值放在倒数第二个的位置,同样的第三遍会将第三大的值放在倒数第三大的值放在倒数第三个位置,依次向后排列,然后就会得到一个完整的排序过后的数组。
public static void sort(int[] arr){
for (int i = arr.length-1 ; i > 0 ; i--){
for (int j = 0 ; j < i ; j++ ){
if (arr[j] > arr[i])
swap(arr,i,j);
}
} }
public static void swap(int[] arr,int i ,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
总体来说,冒牌排序如下几步
判断数组的左右元素的大小,小的放在左边,大的放在右边,这样相当于每一遍,将最大值放在最右边 遍历一遍找到一个最大值,放在最右边
循环N遍,就得到了有序的数组。
冒泡排序最坏情况下,最坏情况就是逆序。时间复杂度也就是 N+N-1+N-2+…+1=N的平方/2 时间复杂度也就是二次方
2.选择排序法
每一次循环,找出最小的元素放在第一位,下一次循环时不看第一个元素,同样找出第一个元素放在第二个位置
public static void sort(int[] arr){
for (int i = 0 ; i < arr.length -1 ; i++ ){
int temp = i;
for (int j = i+1 ; j <arr.length ; j++){
if (arr[j]<arr[temp])
temp = j;
}
exchange(arr,temp,i);
}
}
public static void exchange(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
选择排序法分为如下几步
1.找到数组中最小元素的索引 遍历数组一遍找到最小元素
2.然后将最小元素与排在前几个元素的位置进行交换,最终得到了有序的数组,遍历N遍得到有序数组
选择排序法时时间复杂度是,N-1+N-2+…+1=N的平方/2-N/2,与冒泡排序法相比,时间复杂度更小一点,时间复杂度正属于N的平方
3.插入排序法
把数组分成两部分,左边是排序数组,右边是未排序数组,初始排序数组只有一个元素,也就是整个数组的第一个元素.
然后将未排序数组中的元素从左到右放进已排序的数组中,若未排序数组第一个大于已排序数组最大的元素,则保持位置不动,相当于放进了已排序数组的最右端,若未排序数组第一个小于已排序数组最大的元素,则开始与左边的元素交换,直到这个元素不大于右边的元素位置。
算法描述
①第i次循环时,默认前面i个元素为有序的,将后面的元素 ai后面,像前面插入,如何插入呢?
用待插入的数据与已插入的数据的最大值进行比较,(也可以最小值比较,算法要重新设计)
若是待插入的值较大,则不要动,若是待插入的值较小,往前排列直到待插入的值大于前面的元素的值
使前面的数据依然使有序的,当i等于数组的长度-1时,排序完成。
public static void sort(int[] arr){
int j;
for (int i = 1; i < arr.length ; i++){
int temp = arr[i];
//注意是和arr[j-1]比较,不是和arr[j]进行比较
//因为第一次循环的时候i=j,arr[i] = temp 所以temp = arr[j] 所有相当于和自己进行比较
//也正是这个原因比较也只能采用temp而不能采用arr[i],因为在下面的循环中会调动arr[i],也就是arr[i]是变化的
for (j = i ; j > 0 && temp<arr[j-1] ; j--){
arr[j] = arr[j-1]; // 若不符合要求,则根据要求 对元素的位置进行变动
}
arr[j] = temp; // 将指定的元素插入到指定的位置
}
}
插入排序共分为以下几步
数组分为已排序和未排序,外层循环将数组未排序元素放进已排序元素
若不能直接放进已排序数组,则需要循环来确定放进已排序数组中的位置。
我们来分析一下最坏时间复杂度。
最坏情况下,每次循环都要进行 i-1次操作,
时间复杂度为1+2+3+…+N-1=N的平方/2-N/2,同样也属于N的平方
最好情况下,每次循环只进行 1次操作
时间复杂度为N-1 时间复杂度属于N
因为插入排序过程中,需要往已排序的数组插入数据,这个过程需要查找,我们常用的查找算法就是暴力算法,即遍历一边数组,但是我们可任意才用更优的算法,就比如二分查找,因为前面数据是有序的,所以可以才用二分查找。这样与暴力法相比,运行时间又会减少,所以整个算法的时间复杂度会更好一点。这也就是插入排序中的二分法插入法。
二、高级排序
4.希尔排序法(插入排序的改进版)
分为如下几步:
1.选定好一个增长量h,按照增长量作为分组的依据,对数据进行分组。
2.分别对每一组数据进行插入排序
3.减小增长率,重复操作,直到增长量为1,就得到最终有序的数组。
增长量是影响希尔排序时间复杂度的关键因素。这里采用的增长量是5,2,1,数组长度是10,采用不同长度的数组,增长量不同,时间复杂度会有很大变化的。
public static void sort(int[] arr){
int h = arr.length/2;
int j;
while (h>=1){
for (int i = h ; i < arr.length ; i++){
int temp = arr[i];
for (j = i ; j >= h && temp < arr[j-h] ;j -= h){
arr[j] = arr[j - h];
}
arr[j] = temp;
}
h /= 2;
}
}
该排序方法时间复杂度的计算很难,所有采用估算法,我们利用程序对十万个数据进行排序,查看所需要的时间。
package test;
import java.util.Arrays;
public class test {
public static void sort(int[] arr){
int h = arr.length/2;
int j;
while (h >= 1){ //划分数组,在此希尔排序中,增量定为h,且以半数不断减少,在此依次是,5,2,1
// 最外层循环共分为三次 ,每次h的值分别是 5 ,2 ,1
// 然后进行以增量h分别进行 插入排序
for (int i = h ; i < arr.length ; i++){
int temp = arr[i];
for (j = i ; j >= h && temp < arr[j-h] ; j -= h )
arr[j] = arr[j-h];
arr[j] = temp;
}
h /= 2;
}
}
public static void sort2(int[] arr){
int j;
for (int i = 1; i < arr.length ; i++){
int temp = arr[i];
//注意是和arr[j-1]比较,不是和arr[j]进行比较
//因为第一次循环的时候i=j,arr[i] = temp 所以temp = arr[j] 所有相当于和自己进行比较
//也正是这个原因比较也只能采用temp而不能采用arr[i],因为在下面的循环中会调动arr[i],也就是arr[i]是变化的
for (j = i ; j > 0 && temp<arr[j-1] ; j--){
arr[j] = arr[j-1];
}
arr[j] = temp;
}
}
public static int[] getNums(int n){
int[] nums = new int[100000];
for (int i = 0 ; i < nums.length ; i++ ) {
nums[i] = (int)(Math.random()*100000);
}
return nums;
}
public static void main(String[] args){
int[] nums1 = getNums(100000); // 随机生成十万个数据
long startTimes = System.currentTimeMillis();
sort(nums1);
long endTimes = System.currentTimeMillis();
System.out.println(Arrays.toString(nums1));
System.out.println("希尔排序方法运行时间为"+(endTimes-startTimes)+"ms");
int[] nums2 = getNums(100000);
long startTimes2 = System.currentTimeMillis();
sort2(nums2);
long endTimes2 = System.currentTimeMillis();
System.out.println(Arrays.toString(nums2));
System.out.println("插入排序方法运行时间为"+(endTimes2-startTimes2)+"ms");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZOGzSjbc-1599033173440)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200829211734919.png)]
运行结果,我们发现希尔排序和插入排序的时间复杂度相差很多。希尔排序运行时间是16ms,插入排序是768毫秒。
希尔排序算法的时间复杂度
最好情况,时间复杂度为NlogN
最坏情况,时间复杂度为N的平方
希尔排序的时间复杂度受序列增量影响很大,所以希尔排序的时间复杂度不太确定。
5.归并排序
递归
递归的深度不能太深,因为递归过程中,每一次递归都会占用一块内存,深度太深,会把内存占满,导致死机。
递归的好处是把问题丢给算法自己解决,使问题简单化。
分治法,将数据分开,然后进行操作
首先要分,分半,一直分到子数组中只有一个元素,这样也就可以说每个数组都是有序的,因为每个数组中只有一个元素,然后就开始对数组进行处理,对有序数组进行排序,然后将每个只含有一个元素的数组进行归并,数组不断合并,每次合并得到的都是有序数组
首先第一步 对原数组中数据进行分组,然后对分组之后数组进行排序
private static void mergeSort(int[] arr, int left, int right) {
if (left < right){
int mid = (left + right)/2; // 在数组中间进行分开,以中点为界,将数组分为两个数组
mergeSort(arr,left,mid); // 对左边数据进行排序,递归
mergeSort(arr,mid+1,right); // 对右边数据进行排序,递归
merge(arr,res,left,mid+1,right); //对数据进行归并排序,递归
}
}
public static void mergeSort(int[] arr){
int[] res =new int[arr.length]; // 对整个数组进行排序
mergeSort(arr,res,0,arr.length-1);
}
private static void merge(int[] arr, int[] res, int leftPos, int rightPos, int rightEnd) {
int leftEnd = rightPos-1;
int temPos = leftPos;
int numElements= rightEnd-leftPos+1; // 因为需要递归,所以需要需要动态确定数组的长度,右端数组终点减左端数组起点再加一
while(leftPos <= leftEnd && rightPos <= rightEnd) // 对数组进行操作 对数组进行归并
if (arr[leftPos] <= arr[rightPos]) // 若左边数组小,则把左边数组的元素放在备用数组中
res[temPos++] = arr[leftPos++];
else
res[temPos++] = arr[rightPos++]; // 若右边数组小,则把右边数组的元素放在备用数组中
while (leftPos <= leftEnd ){
res[temPos++] = arr[leftPos++]; // 若左边数组元素没有全放进备用数组中,右边数组全放进备用数组中了
} // 就将左边数组元素全部放在备用数组后面
while (rightPos<=rightEnd) // 若右边数组元素没有全放进备用数组中,左边数组全放进备用数组中了
res[temPos++] = arr[rightPos++]; // 就将右边数组元素全部放在备用数组后面
for (int i=0; i< numElements;i++,rightEnd--) // 将备用数组中的元素放进原数组中
arr[rightEnd] = res[rightEnd];
}
时间复杂度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zsQD3c7f-1599033173442)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200830170216624.png)]
我们可以看到,归并排序与希尔排序时间均为15ms左右,所以可以证明他们的时间复杂度为NlogN,与上面相比同样比插入排序速度快很多。
6.快速排序
同样是分组的原理,在数据中找出中间值,然后将大于该中间值的数据分为一组,小于这个元素放在一组,大的数组放在中间值右边,小的元素放在中间值左边,然后对每个小数组进行在排序,最终就会得到有序的数组。
算法描述:
①选取数组中的一个值作为基准值,可以是任意值,一般来说选第一个值,或中间值,然后空出这个位置的值
②在数组后端找到小于基准值的元素放在数组前端。这样,把大的数据放在后面,小的数据放在前面。
③重复执行之前的操作,可以分别在 4 2 1这样的位置执行以上的操作,经过logN次操作基本变换完成。这个过程要用递归。
private static int partition(int[] arr, int low, int high) {
//指定左指针i和右指针j
int i = low;
int j= high;
//将第一个数作为基准值。挖坑
int x = arr[low];
//使用循环实现分区操作
while(i!=j){//5 8
//从右向左移动j,找到第一个小于基准值的值 arr[j],找左边的第一个坑
while(arr[j]>=x && i<j){
j--;
}
//2.将右侧找到小于基准数的值加入到左边的(坑)位置, 左指针想中间移动一个位置i++
if(i<j){
arr[i] = arr[j];
i++;
}
//3.从左向右移动i,找到第一个大于等于基准值的值 arr[i],找右边的第一个坑
while(arr[i]<x && i<j){
i++;
}
//4.将左侧找到的打印等于基准值的值加入到右边的坑中,右指针向中间移动一个位置 j--
if(i<j){
arr[j] = arr[i];
j--;
}
}
//使用基准值填坑,这就是基准值的最终位置
arr[i] = x;//arr[j] = y;
//返回基准值的位置索引
return i; //return j;
}
private static void quickSort(int[] arr, int low, int high) {//???递归何时结束
if(low < high){
//分区操作,将一个数组分成两个分区,返回分区界限索引
int index = partition(arr,low,high);
//对左分区进行快排
quickSort(arr,low,index-1);
//对右分区进行快排
quickSort(arr,index+1,high);
}
}
public static void Sort(int[] arr) {
int low = 0;
int high = arr.length-1;
quickSort(arr,low,high);
}
快速排序的不同种实现方法
static int getMiddle(int []array,int lo,int hi) {
10 //固定的切分方式
11 int key=array[lo];
12 while(lo<hi){
13 //从后半部分向前扫描
14 while(array[hi]>=key&&hi>lo){
15 hi--;
16 }
17 array[lo]=array[hi];
18 //从前半部分向后扫描
19 while(array[lo]<=key&&hi>lo){
20 lo++;
21 }
22 array[hi]=array[lo];
23 }
24 array[hi]=key;
25 return hi;
26 }
我们也可以采用list集合来进行操作,因为list集合对数组操作会快很多
public static void sort(List<Integer> list){
if (list.size() > 1){
List<Integer> smaller = new ArrayList<>();
List<Integer> normal = new ArrayList<>();
List<Integer> larger = new ArrayList<>();
Integer chosenItem =list.get(list.size()/2);
for (Integer i: list){
if (i>chosenItem)
larger.add(i);
else if (i<chosenItem)
smaller.add(i);
else
normal.add(i);
}
sort(smaller);
sort(larger);
list.clear();
list.addAll(smaller);
list.addAll(normal);
list.addAll(larger);
}
}
时间复杂度
最好情况,NlogN
最坏情况,N的平方
这里上面的快速排序和下面的快速排序时间复杂度也是不同的,所以每种排序算法的不同实现方法也会有不同的效果,我们可以来演示一下。我们用十万个随机数来进行测试。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AKfkAfIq-1599033173443)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200830204154842.png)]
我们发现,采用List集合快速排序比没有采用的快速排序速度快了接近一倍。
当数据数量较多,数据随机排列时,快速排列是“快速的”,当数据数量较少时或者基准值选取不合适时,快速排序较慢,也就是说快速排序时不稳定的。
7.堆排序
该排序利用到完全二叉树,这里先不谈。
三、Arrays类中的排序方法sort
Java中自带的一个Arrays类,这个类中存在一个排序方法,那么这个类中的排序方法采用的是那种排序方法呢?
我们可以看一下API
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jre0YJXC-1599033173445)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200830173926382.png)]
这里说,他们采用的是Vladimir Yaroslavskiy、Jon Bentley和Joshua Bloch的双轴快速排序算法,这是JDK8的文档说明
我们可以看一下最新的JDK14中的Arrays类中的排序采用的是什么排序。
JDK14中才用的是多种排序。
1.当元素数量少于47个时,采用插入排序
2.元素数量大于等于47个,小于等于286个时,采用的是经典快速排序。
3.元素数量大于286个,如果数组的连续性好,采用归并排序,如果连续性不好,采用的是双轴快速排序。