【数据结构】快速排序(详解)

目录

快速排序

历史:

基本思想:

主框架:

下面解释实现单次排序的几种版本:

1.Hoare版本

2. 挖坑法

3. 前后指针法

快速排序的实现包括递归与非递归:

1. 递归实现:(即开头的基本框架)

2. 非递归:(迭代)

下面我们通过栈来演示非递归实现快速排序:


快速排序

历史:

快速排序是Hoare(霍尔)于1962年提出的一种二叉树结构的交换排序方法

基本思想:

任取待排序元素序列中的一个元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后在左右子序列重复该过程,直到所有元素都排序在相应位置上为止

主框架:

上述为快速排序递归实现的主框架,我们可以发现它和二叉树的前序遍历十分相似 

下面解释实现单次排序的几种版本:
1.Hoare版本

单趟动图演示:

步骤:

  1. 选择基准值:通常选择序列的第一个元素或最后一个元素作为基准值。

  2. 设置两个int变量记录数组下标:一个记录序列的开始的下标(left),另一个记录序列的末尾的下标(right)。

  3. 开始分区

    • 先让right++,直到找到一个比基准值大的元素。
    • 再让left--,直到找到一个比基准值小的元素。
    • 交换这两个元素的位置,并继续right++。
    • 重复上述步骤,直到left >= right。
    • 注意:right先加,保证下一步骤与基准值交换的值小于基准值
  4. 交换基准值与right位置的值:此时,基准值左边的所有元素都比基准值小,基准值右边的所有元素都比基准值大。

代码实现:

int PartSort1(int* a, int left, int right) {
	int key = left;
	while (left < right) {
		while(a[right] >= a[key]&&left < right) {
			right--;
		}
		while(a[left] <= a[key]&&left < right) {
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[key], &a[right]);
	return left;
}
2. 挖坑法

单趟动图演示:

步骤:

  1. 选择基准元素:在待排序的序列中,选择一个元素作为基准元素(key)。这个元素可以是序列的第一个元素、最后一个元素或者任意一个元素,甚至是随机选取的一个元素。

  2. 挖坑:将基准元素从原位置移除,形成一个“坑”(hole)。

  3. 填坑:从序列的一端开始(可以是左端或右端),将遇到的第一个比基准元素小(或大)的元素填入坑中,并形成一个新的坑。这里有两种情况:

    • 如果从右向左遍历,遇到比基准元素小的元素,则将其填入坑中,并继续从左向右遍历。这里代码实现选择该情况
    • 如果从左向右遍历,遇到比基准元素大的元素,则将其填入坑中,并继续从右向左遍历。
  4. 此时,基准元素左边的所有元素都小于基准元素,右边的所有元素都大于或等于基准元素。

代码实现:

int PartSort2(int* a, int left, int right) {
	int x = a[left];
	int key = left;
	while (left < right) {
		while (a[right] >= x && left < right) {
			right--;
		}
		a[key] = a[right];
		key = right;
		while (a[left] <= x && left < right) {
			left++;
		}
		a[key] = a[left];
		key = left;
	}
	a[key] = x;
	return key;
}
3. 前后指针法

单趟动图演示:

基本步骤:

  • 从待排序的数组中选择一个元素(key)作为基准。通常选择第一个或最后一个元素作为基准,但也可以随机选择。
  • 设置两个下标,一个记录序列的开始的下标(pre),即pre=left,另一个记录pre下一个元素的下标的下标(cur),即cur=pre+1;
  • 当(cur<=right)时,循环执行:cur找比key小的元素,如果a[cur]<=a[key],交换a[cur]和a[pre],否则pre++,每次循环cur++

代码实现:

int PartSort3(int* a, int left, int right) {
	int key = left;
	int pre = left;
	int cur = pre + 1;
	while (cur <= right) {
		if (a[cur] <= a[key] && ++pre != cur) {
			swap(&a[cur], &a[pre]);
		}
		cur++;
	}
	swap(&a[key], &a[pre]);
	return pre;
}
快速排序的实现包括递归与非递归:
1. 递归实现:(即开头的基本框架)
void QuickSort(int* a, int left, int right) {
	if (left >= right) {
		return;
	}
	//int key = PartSort1(a, left, right);
	//int key = PartSort2(a, left, right);
	int key = PartSort3(a, left, right);
	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
}
2. 非递归:(迭代)

在非递归版本中,我们使用栈(或队列)来保存待排序的子数组的起始和结束索引,而不是直接递归调用;栈相当于前序遍历,而队列相当于层序遍历

下面我们通过栈来演示非递归实现快速排序:

思路与步骤:

  1. 循环处理栈:当栈不为空时,执行以下步骤:

  • 弹出栈顶的两个元素,它们分别表示当前子数组的起始和结束索引。
  • 如果起始索引等于结束索引(即子数组只有一个元素),那么直接跳过,不需要排序。
  • 调用PartSort3函数对当前子数组进行划分,并返回划分后的基准索引key
  • 弹出之前压入的起始和结束索引(因为已经处理过这个子数组了)。
  • 根据key的位置,决定下一步的操作:
    • 如果key等于起始索引left,说明基准左边的元素都已经排好序,只需要对基准右边的元素进行排序,所以将key + 1right压入栈中。
    • 如果key等于结束索引right,说明基准右边的元素都已经排好序,只需要对基准左边的元素进行排序,所以将leftkey - 1压入栈中。
    • 如果key既不等于left也不等于right,说明基准左右两边都有需要排序的元素,所以将leftkey-1key+1right的子数组分别压入栈中
  • 2.完成:当栈为空时,表示所有子数组都已排序完成,此时整个数组也已排序完成。

代码实现:

typedef int StackNode;
typedef struct Stack {
	StackNode* a;
	int real;
	int capacity;
}Stack;
void InitStack(Stack* p) {
	assert(p);
	p->a = NULL;
	p->capacity = p->real = 0;
}
//扩容
void expansion(Stack*p) {
	int newcapacity = p->capacity == 0 ? 4 : 2 * p->capacity;
	StackNode* tmp = (StackNode*)realloc(p->a, sizeof(StackNode) * newcapacity);
	if (tmp == NULL) {
		perror("expansion::realloc::NULL");
		exit(0);
	}
	p->a = tmp;
	p->capacity = newcapacity;
}
//入栈
void Push(Stack*p,int x) {
	assert(p);
	if (p->capacity == p->real) {
		expansion(p);
	}
	p->a[p->real++] = x;
}
//出栈
void Pop(Stack* p) {
	assert(p);
	p->real--;
}
void StackDestroy(Stack* p) {
	assert(p);
	free(p->a);
	p->a = NULL;
	p->capacity = p->real = 0;
}
//快排代码实现
void QuickSortNonR(int* a, int left, int right) {
	if (left >= right) {
		return;
	}
	Stack stack;
	InitStack(&stack);
	Push(&stack, left);
	Push(&stack, right);
	while (stack.real) {
		left = stack.a[stack.real - 2];
		right = stack.a[stack.real - 1];
		if (left == right) {
			Pop(&stack);
			Pop(&stack);
			continue;
		}
		int key = PartSort3(a, left, right);
		Pop(&stack);
		Pop(&stack);
		if (key == left) {
			Push(&stack, key + 1);
			Push(&stack, right);
			continue;
		}
		if (key == right) {
			Push(&stack, left);
			Push(&stack, key - 1);
			continue;
		}
		Push(&stack, key + 1);
		Push(&stack, right);
		Push(&stack, left);
		Push(&stack, key - 1);
	}
	StackDestroy(&stack);
}

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据结构算法是计算机科学中非常重要的两个概念。数据结构是一种组织和存储数据的方式,而算法是解决问题的步骤和方法。在计算机科学中,有许多经典的数据结构算法,被广泛应用于各种领域。 以下是十大经典数据结构算法的简要介绍: 1. 数组(Array):是一种线性数据结构,可以存储相同类型的元素。数组的访问速度快,但插入和删除操作较慢。 2. 链表(Linked List):也是一种线性数据结构,由节点组成,每个节点包含数据和指向下一个节点的指针。链表适用于频繁的插入和删除操作。 3. 栈(Stack):是一种后进先出(LIFO)的数据结构,只能在栈顶进行插入和删除操作。 4. 队列(Queue):是一种先进先出(FIFO)的数据结构,只能在队尾插入,在队头删除。 5. 树(Tree):是一种非线性数据结构,由节点和边组成。树有许多种类型,如二叉树、平衡树、堆等。 6. 图(Graph):也是一种非线性数据结构,由节点和边组成。图可以表示各种实际问题,如网络、社交关系等。 7. 哈希表(Hash Table):使用哈希函数将数据存储在数组中,可以快速查找、插入和删除数据。 8. 排序算法(Sorting Algorithm):如冒泡排序、插入排序、快速排序等,用于将数据按照某种规则进行排序。 9. 查找算法(Search Algorithm):如线性查找、二分查找等,用于在数据集中查找特定元素。 10. 图算法(Graph Algorithm):如最短路径算法(Dijkstra算法)、深度优先搜索算法(DFS)、广度优先搜索算法(BFS)等,用于解决图相关的问题。 以上是十大经典数据结构算法的简要介绍,每个数据结构算法都有其特点和适用场景,深入学习它们可以帮助我们更好地理解和解决实际问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值