java数据结构与算法2---数组及其算法(2)

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 知飞翀

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值