一开始只想贴代码,但是后来发现还是有人看的。所以觉得还是有必要花点时间去写写。
先定义一个用于交换两个数的函数吧。
#if 0
inline void swap(int& a, int& b)
{
int c = a;
a = b;
b = c;
}
#else
//
inline void swap(int& a, int& b)
{
if (a != b)
{
a = a^b;
b = a^b;
a = a^b;
}
}
#endif
用于获得一段代码执行时间的宏,限于windows系统,其他系统可以用相应的获取时间的函数代替GetTickCount()。
/*
*获取一段代码(func)执行的时间
*/
#define GetTm(func, t) \
do{ \
DWORD t1, t2; \
t1 = GetTickCount(); \
func; \
t2 = GetTickCount(); \
t = (t2 - t1); \
}while(0)
第一个排序算法,冒泡:
简单的冒泡的过程像是在水里有个气泡,他一直往上冒一样。
我就不多说了。直接看代码:
/*
简单冒泡排序
*/
void Buble(int a[], int n)
{
assert(NULL != a && n > 1);
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n-i-1; j++)
{
if (a[j] > a[j+1])
{
swap(a[j], a[j+1]);
}
}
}
}
直接来说优化。我们记录下最后一次发生交换的地方,之后的都是有序的
/*
* 改进,记录最后一次交换的位置,则这个位置之后的是有序的,无交换则全有序
*/
void Buble1(int a[], int n)
{
assert(NULL != a && n > 1);
int pos = 0;
for (int i = n-1; i > 0; )
{
pos = 0;
for (int j = 0; j < i ; j++)
{
if (a[j] > a[j + 1])
{
pos = j;//记录最后一次交换的位置
swap(a[j], a[j + 1]);
}
}
i = pos;//之后的有序,不用再遍历
}
}
第二种优化,我们可以同时首尾都进行冒泡,一次找到一个最大的,一个最小的。可能我写的代码有问题。经过我多次试验,效率反而下降。。或者数据很不凑巧。
/*
首尾同时进行冒泡排序。
*/
void Buble2(int a[], int n)
{
assert(NULL != a && n > 1);
int high = n - 1;
int low = 0;
int i, j;
while (low < high)
{
//找到最大的。
for (j = low; j < high; j++)
{
if (a[j] > a[j+1])
{
swap(a[j], a[j+1]);
}
}
high--;
//找到最小的
for ( i = high; i > low; i--)
{
if (a[i] < a[i-1])
{
swap(a[i], a[i-1]);
}
}
low++;
}
}
选择排序:
选择排序的思想,一次选出一个最大的数,或者找到最小的数。
代码
/*
选择排序
*/
void ChooseSort(int a[], int n)
{
assert(NULL!=a && n>1);
for (int i = 0; i < n-2; i++)
{
for (int j = i+1; j < n-1; j++)
{
if (a[i] > a[j])
{
swap(a[i], a[j]);
}
}
}
}
优化的话,一次其实也可以找到一个最大值,一个最小值,我实际测试的时候,发现还是有很大提升的。
/*
选择排序,每次找到最大和最小
*/
void ChooseSort1(int a[], int n)
{
assert(NULL != a && n>1);
int min = a[0], max = a[0];
int minPos =0, maxPos = 0;
int i, j;
for ( i = 0; i < n-i-1; i++)
{
min = a[i];
max = a[i];
for ( j = i; j < n-i-1; j++)
{
if (min > a[j])
{
min = a[j];
minPos = j;
}
if (max < a[j])
{
max = a[j];
maxPos = j;
}
}
swap(a[i], a[minPos]);
swap(a[j], a[maxPos]);
}
}
插入排序
数组前面的是有序的。后面的数,插入到前面有序部分相应的位置。
/*
*插入排序
*/
void InsertSort(int a[], int n)
{
assert(NULL != a && n>1);
int i, j,temp;
for (i = 1; i < n; i++)
{
temp = a[i];
for (j = i - 1; j >= 0 && temp < a[j]; j--)
{
a[j+1] = a[j];
}
a[++j] = temp;
}
}
优化的话。。没想到啥。。。
快速排序:
快速排序的思想是一次确定一个数的位置,使得他左边比他小,右边比他大。
/*
快速排序
*/
void QuikSort(int a[], int n)
{
assert(NULL != a );
int i = 0, j = n - 1;
int temp = a[0];
if (n <= 1)
{
return;
}
while (i < j)
{
while (i<j && temp <= a[j])
{
j--;
}
a[i] = a[j];
while (i<j && temp >= a[i])
{
i++;
}
a[j] = a[i];
}
a[i] = temp;
QuikSort(a, i );
QuikSort(a + i + 1, n - i -1 );
}
对快排的优化,可以从下面的几点出发,
1、与关键码(枢轴)相同的数,可以移动到中间,下次不再参与递归。
2、枢轴的选择很重要。如果每次都选择第一个,则有可能出现一种情况,那就是已经是一个有序的数组的话,快排退化到冒泡。。。所以选取枢轴,尽量让这个枢轴就是靠近数据中间位置的数,这个可以选择数组两端和中间的三个数做比较,选取第二大的(中间数)作为枢轴,但是如果数组很大,这估计也不能得到比较好的效果,所以还有一种方法,选取前三个数,取中数,中间三个数,去中数,最后的三个数,取中数,然后在这三个中数中再选取中数,以最后选取出来的这个中数作为枢轴。其实觉得还可以把数组分成三部分,0-1/4,1/4-3/4,3/4-1,主要是可以用位移运算才这么分。然后分别取三个区间的中位数比较,最后再取三个中位数的中位数作为枢轴。
例如 13,33,3,4,43,55,44,23,66,88,99
第一种选取13,55, 99,比较获得枢轴为55。
第一种选取13,33, 3,选取13。中间三个43,55,44选取44。 后面三个66,88,99,选取88,然后13,44,88再比较,最终选取44作为枢轴。
第三种选取13,43,44, 66,99,最后比较取 44 这个中间数作为枢轴。
3、当一次比较之后分出的子区间的个数很小。接近于8个左右,可以采用插入排序对这个子区间进行排序。
4、递归改为循环,明显第一个递归很好改,但是第二个递归就不那么好改了。
基于以上说明。贴出代码
//获得三个数中第二大的数的位置
int GetMidNumPos(int arr[],int a, int b,int c)
{
return (arr[a] < arr[b]) ? \
((arr[b] < arr[c]) ? \
b : ((arr[a] < arr[c]) ? c : a)) : \
((arr[a] < arr[c]) ? \
a : ((arr[b] < arr[c]) ? c : b));
}
void QuikSort1(int a[], int n)
{
assert(NULL != a);
if (n <= 1)
{
return;
}
int i, j, temp;
if (n <= 7)
{
//个数小于7则插排
for ( i = 1; i < n; i++)
{
temp = a[i];
for ( j = i - 1; j >= 0 && temp < a[j]; j--)
{
a[j + 1] = a[j];
}
a[++j] = temp;
}
return;
}
else if ( n <= 40 )
{
//三数取中
int mid = GetMidNumPos(a, 0, n >> 1, n - 1);
temp = a[mid];
}
else
{
//9数取中
int mid, mid1, mid2, mid3;
mid1 = GetMidNumPos(a, 0, 1, 2);
mid2 = GetMidNumPos(a, n >> 1 - 1, n >> 1, n >> 1 + 1);
mid3 = GetMidNumPos(a, n - 3, n - 2, n-1);
mid = GetMidNumPos(a, mid1, mid2, mid3);
temp = a[mid];
}
int h, t;
h = i = 0;
t = j = n - 1;
while (true)
{
while (i<=j && temp >= a[i])
{
//先把与枢轴相同的元素移到左边
if (temp == a[i])
{
swap(a[i], a[h]);
h++;
}
i++;
}
while (i<=j && temp <= a[j])
{
//先把与枢轴相同的元素移到右边
if (temp == a[j])
{
swap(a[j], a[t]);
t--;
}
j--;
}
if (i>j)
{
break;
}
swap(a[i], a[j]);
i++;
j--;
}
//把移动到两边与枢轴相同的数移动到中间。下次不再递归
for (int k = 0; k < h; k++)
{
i--;
swap(a[k], a[i]);
}
for (int k = n-1; k >t ; k--)
{
j++;
swap(a[k], a[j]);
}
QuikSort1(a, i);
QuikSort1(a + j + 1 , n - j - 1 );
}
做了一些改变之后的快排:
void QuikSort2(int arr[], int len)
{
assert(NULL != arr);
int n = len;
int i, j, ti, tj, steps, temp;
int* a = arr;
int h, t, k;
int mid, mid1, mid2, mid3;
while (len > 1)
{
n = len;
if (n <= 8)
{
for (i = 1; i < n; i++)
{
temp = a[i];
for (j = i - 1; j >= 0 && temp < a[j]; j--)
{
a[j + 1] = a[j];
}
a[++j] = temp;
}
return;
}
else if (n <= 40)
{
mid = GetMidNumPos(a, 0, n >> 1, n - 1);
temp = a[mid];
}
else
{
//数组分成三个区间,三个区间的中位数的中位数作为枢轴
int num1 = n >> 2;
int num2 = n >> 1;
mid1 = GetMidNumPos(a, 0, num1 >> 1, num1 - 1);
mid2 = GetMidNumPos(a, num1, num2, num1 + num2);
mid3 = GetMidNumPos(a, num1 + num2 + 1, num1 + num2 + num1>>1 + 1, n - 1);
mid = GetMidNumPos(a, mid1, mid2, mid3);
temp = a[mid];
}
h = i = 0;
t = j = n - 1;
while (true)
{
while (i <= j && temp >= a[i])
{
if (temp == a[i])
{
swap(a[i], a[h]);
h++;
}
i++;
}
while (i <= j && temp <= a[j])
{
if (temp == a[j])
{
swap(a[j], a[t]);
t--;
}
j--;
}
if (i > j)
{
break;
}
swap(a[i], a[j]);
i++;
j--;
}
ti = i;
//移动次数可以调整
steps = (h < (ti - h)) ? h : (ti - h);
for ( k = 0; k < steps; k++)
{
--ti;
swap(a[k], a[ti]);
}
len = i - h + 1;
tj = j;
steps = (n - 1 - t) < (t - j) ? (n - 1 - t) : (t - j);
for ( k = 0; k <steps; k++)
{
++tj;
swap(a[n - 1 - k], a[tj]);
}
//第一层递归改用循环
// QuikSort2(a, i);
//len = i;
QuikSort2(a + j + 1, n - j - 1);
}
}
经过我多次实践,实践上大部分时候第三种改过的反而没有第二种快。。。我觉得主要原因可能是三目运算?:,还有取枢轴的时候计算多了一点。。
以上说明,代码如有问题,请斧正,感激不尽。
谢谢。