java数据结构与算法2---数组及其算法(2)
数组常用排序算法(续)
快排算法(快速排序)
问题引入:
问题一:给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
问题二 (荷兰国旗问题): 给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
思路:以上图为例,现有数组{1,3,2,5,4},num=3,问题一中我们假设存在一个<=num的区域(放置小于等于num的值)。存在一个指针指向数组的第一个位置,当指针所指的值不符合<num就指针后移一个,这时p指向3满足小于等于3,这时就将3与<num区的下一位交换位置,并且<num区扩充一位同时p后移一位直至将整个数组遍历结束。通俗的说整个过程好像<num区在推着整个数组向右移动;问题二就稍微复杂一点,这是就要假设存在2个区域(<num区,>num区) 满足那个区就填入那个区,最终=num处于中间位置形成=num区。直观来讲就是<num区和>num区一起夹着数组最终形成<num区---=num区--->num区的布局。下面给出问题二代码:
//荷兰国旗问题代码实现
public static int[] partition(int[] arr, int l, int r, int p) {
int less = l - 1;
int more = r + 1;
while (l < more) {
if (arr[l] < p) {
swap(arr, ++less, l++);
} else if (arr[l] > p) {
swap(arr, --more, l);
} else {
l++;
}
}
return new int[] { less + 1, more - 1 }; //返回=num区的左右两端的索引位置
}
//交换数组两个位置的值
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
快排介绍
传统快排:快排类似于荷兰国旗问题,在传统快排中第一次取num=数组的最后一个值进行partition方法外,之后的递归过程中都是取<num区的末端和>num区的始端做为新的num值分别对<num区、>num区进行partition方法,最终通过不断递归实现快排。就是利用不断的确定=num区来实现的一种排序思想。
随机快排:随机快排是传统快排的优化,由于在传统快排中每次都是区端点位置的值做为num来进行排序,可能存在num的值正好为数组的最大值或最小值等极端值,此时就无法将数组分成较为相等的<num区和>num区,进而也就无法实现master公式T(n)=2T(n/2)+O(n) ---log(2,2)=1 时间复杂度为O(n*logn),为了提高快排的效率,采用随机选取数组值的方法给num赋值,从概率的角度得出平均期望下的时间复杂度为O(n*logn)。随机快排是实际工程中经常使用的算法。
随机快排代码实现(包含传统快排):( 时间复杂度:O(N*logN) 空间复杂度:O(logN) )
//随机快排算法实现
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
//随机选取数组中的值,去除此语句就是传统快排
swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1, r);
}
}
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while (l < more) {
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return new int[] { less + 1, more }; //产生额外空间
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
堆排序
在介绍堆排序之前先来说说完全二叉树,完全二叉树是指从左到右依次补齐的二叉树;满二叉树是完全二叉树的一个特例,它要求二叉树的叶子节点只能且都在同一行上,即排序的行必须全部占满。(以上是个人通俗话语,严格定义还请参考课本)
介绍堆
大根堆:完全二叉树中任意一棵子树最大值都是这棵子树的根(头部)
小根堆:完全二叉树中任意一棵子树最小值都是这棵子树的根(头部)
堆排序:尽管堆是一种二叉树结构,但是堆也可以用数组来表示,也就是将堆的完全二叉树的行排序顺序利用数学转换到数组中表示。如下图:(数组中的0,1,2,3,4,5代表索引位置)
堆排序的实现
1)创建大根堆(或小根堆):创建大根堆的时候就是向数组中插入数据,首先将要插入的数据放在数组第一个null上,通过插入数据的数组索引位置利用左子树2*i+1、右子树2*i+2来确定出此索引对应的父索引,进而判断父索引满足大根要求?若满足则结束;若不满足则将子索引对应数值的最大值与父索引对应的数值交换,并且根据父索引推导出父父索引再进行判断是否满足大根要求——>至全部满足大根要求就结束。
2)取出最大值:根据前面创建的大根堆可知数组arr[0]的数值最大,此时将arr[0]与arr[size-1]交换(size-1为数组末索引),然后将堆的大小减一;由于改变了堆0位置的数值就可能影响整个堆,此时再利用左子树2*i+1、右子树2*i+2来确定左右子树的索引位置,判断是否满足大根堆要求?满足则结束;交换向下继续操作(这一步是从上向下进行,与前面创建大根堆操作相反但原理相同)。
3)循环取最大值:不断的取最大值至堆大小为0结束,最终也就将数据排成按索引从小到大的升序排列。
堆排序代码如下:( 时间复杂度:O(N*logN) 空间复杂度:O(1) )
//堆排序代码实现
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
//创建大根堆
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
//取出堆中最大值
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
排序算法的稳定性分析
排序算法的稳定性介绍:排序前和排序后相等元素的先后顺序保持不变,则此排序算法是稳定的;若先后顺序发生改变,则此排序算法是不稳定的。
稳定性分析
冒泡排序:可以实现稳定排序(需要coding的时候有目的性针对编写实现代码)。
选择排序:不稳定排序,在找到最小值进行交换的过程中将打破相等数值的顺序。
插入排序:可以实现稳定排序。
外排:可以实现稳定排序。
归并排序:可以实现稳定排序。
(随机)快排:不稳定排序,在(随机)选取num的过程中打破稳定性。
堆排序:不稳定排序,在取出最大值的过程中打破稳定性。
排序问题的补充
1)归并排序的额外空间复杂度可以变成O(1),但是非常难,不需要掌握,可以搜 “归并排序内部缓存法”
2)快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜 “01 stable sort”
3)有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变。(问题相当于告诉你快排是稳定的)碰到这个问题,可以怼面试官。
比较器
我们知道在实际的工程中我们会遇到给中实际问题的比较问题,例如:比较同一班级的学生年龄大小、分数高低等等,这种比较是基于自定义类型Student下进行的。面对这种自定义类型的比较,在系统中默认是按照内存地址进行比较的,因此并不能按我们所想的那样比较并正确排序显示出来,这是就需要比较器来完成我们所想了。比较器由程序员自己定义比较的依据,在进行排序的时候将比较器引入进去,那么最终的排序就会按照比较器规定的依据来进行比较排序。下面以Java比较器为例:
//自定义Student类型
public static class Student {
public String name;
public int id;
public int age;
public Student(String name, int id, int age) {
this.name = name;
this.id = id;
this.age = age;
}
}
/*自定义比较器
*实现Comparator中的compare方法
*/
public static class IdAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
//返回值>0则o1在后o2在前;返回值=0顺序不变;返回值<0则o1在前o2在后
return o1.id - o2.id;
}
}
//打印输出
public static void printStudents(Student[] students) {
for (Student student : students) {
System.out.println("Name : " + student.name + ",
Id : " + student.id + ", Age : " + student.age);
}
System.out.println("===========================");
}
//test
public static void main(String[] args) {
Student student1 = new Student("A", 1, 23);
Student student2 = new Student("B", 2, 21);
Student student3 = new Student("C", 3, 22);
Student[] students = new Student[] { student3, student2, student1 };
printStudents(students);
//调用系统sort方法并引入自定义比较器
Arrays.sort(students, new IdAscendingComparator());
printStudents(students);
}
我将在java数据结构与算法2---数组及其算法(3)中介绍数组非基于比较的排序算法—— 桶排序、计数排序、基数排序
敬请关注! 点赞+关注不迷路哟!
谢谢阅读 ---by 知飞翀