快速排序算法 & 面向对象的继承

1、快速排序

在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。

事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。(有二分的思想),步骤为:

  1. 从序列中挑出一个元素,作为"基准"(pivot).
  2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
  3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
它是基于关键字比较的内部排序算法中速度最快者,效率最高。平均时间复杂度O(nlogn)。
Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?
答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,
所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。 
#include <iostream>
using namespace std;

// 分类 ------------ 内部比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)
// 稳定性 ---------- 不稳定

void Swap(int A[], int i, int j)
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

int Partition(int A[], int left, int right)  // 划分函数
{
    int pivot = A[right];               // 这里每次都选择最后一个元素作为基准
    int tail = left - 1;                // tail为小于基准的子数组最后一个元素的索引
    for (int i = left; i < right; i++)  // 遍历基准以外的其他元素
    {
        if (A[i] <= pivot)              // 把小于等于基准的元素放到前一个子数组末尾
        {
            Swap(A, ++tail, i);
        }
    }
    Swap(A, tail + 1, right);           // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
                                        // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
    return tail + 1;                    // 返回基准的索引
}

void QuickSort(int A[], int left, int right)
{
    if (left >= right)
        return;
    int pivot_index = Partition(A, left, right); // 基准的索引
    QuickSort(A, left, pivot_index - 1);
    QuickSort(A, pivot_index + 1, right);
}

int main()
{
    int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 }; // 从小到大快速排序
    int n = sizeof(A) / sizeof(int);
    QuickSort(A, 0, n - 1);
    cout<<"快速排序结果:"<<endl;
    for (int i = 0; i < n; i++)
    {
        cout<<A[i]<<endl;
    }
    return 0;
}
拓展::
八个内部排序算法;参考排序算法的总结


简单写下冒泡排序;

基本思想:

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。

即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

#include <iostream>
using namespace std;

void bubbleSort(int A[],int n)
{
for (int i=0;i<n-1;++i)
{
    for (int j=0;j<n-i-1;++j)
    {
        if (A[j]>A[j+1])  //从小到大排序
        {
            int tmp=A[j]; A[j]= A[j+1]; A[j+1]=tmp;
        }
    }
}
}

int main()
{
    int a[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 }; // 从小到大快速排序
    int n = sizeof(a) / sizeof(int);
    bubbleSort(a,n);
    for (int i=0;i<n;++i)
        cout<<a[i]<<endl;
    return 0;
}
2、对象的继承理解

介绍了在 C ++ 中继承的概念、基本使用方法、三种继承方式、多继承、二义性、虚基类、构造函数与析构函数等相关知识。

基本概念

继承
: 类的继承,就是新的类从已有类那里得到已有的特性。原有的类称为基类或父类,产生的新类称为派生类或子类。

基本语法

派生类的声明:

class 派生类名:继承方式 基类名1, 继承方式 基类名2,...,继承方式 基类名n
{
    派生类成员声明;
};


在 c++ 中,一个派生类可以同时有多个基类,这种情况称为多重继承。如果派生类只有一个基类,称为单继承。派生类继承基类中除构造和析构函数以外的所有成员。

类的继承方式

C++类继承方式
继承方式规定了如何访问基类继承的成员。继承方式有public, private, protected。如果不显示给出继承方式,默认为private继承。继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限。

  • 公有继承
    当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。即基类的公有成员和保护成员被继承到派生类中仍作为派生类的公有成员和保护成员。派生类的其他成员可以直接访问它们。无论派生类的成员还是派生类的对象都无法访问基类的私有成员。
    • 私有继承
      当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可访问。基类的公有成员和保护成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类外部通过派生类的对象无法访问。无论是派生类的成员还是通过派生类的对象,都无法访问从基类继承的私有成员。通过多次私有继承后,对于基类的成员都会成为不可访问。因此私有继承比较少用。
    • 保护继承
      保护继承中,基类的公有成员和私有成员都以保护成员的身份出现在派生类中,而基类的私有成员不可访问。派生类的其他成员可以直接访问从基类继承来的公有和保护成员,但是类外部通过派生类的对象无法访问它们,无论派生类的成员还是派生类的对象,都无法访问基类的私有成员。

派生类的构造函数

  1. 派生类中由基类继承而来的成员的初始化工作还是由基类的构造函数完成,派生类中新增的成员在派生类的构造函数中初始化
    派生类构造函数的语法:
派生类名::派生类名(参数总表):基类名1(参数表1),基类名(参数名2)....基类名n(参数名n),内嵌子对象1(参数表1),内嵌子对象2(参数表2)....内嵌子对象n(参数表n)
{
    派生类新增成员的初始化语句;
}

 
 
 
 
 
 
 
 
 
 
 
 
 
 

注:构造函数的初始化顺序并不以上面的顺序进行,而是根据声明的顺序初始化。

  1. 如果基类中没有不带参数的构造函数,那么在派生类的构造函数中必须调用基类构造函数,以初始化基类成员。
    1. 派生类构造函数执行的次序:
      1. 调用基类构造函数,调用顺序按照它们 被继承时声明的顺序 (从左到右);
      2. 调用内嵌成员对象的构造函数,调用顺序按照它们在类中声明的顺序;
      3. 派生类的构造函数体中的内容。

派生类的析构函数

派生类的析构函数的功能是在该对象消亡之前进行一些必要的清理工作,析构函数没有类型,也没有参数。析构函数的执行顺序与构造函数相反。

实例:

     
     
1
 
#include <iostream>
#include <time.h>
using namespace std;
// 基类 B1
class B1
{
public:
    B1(int i)
    {
        cout<<"constructing B1 "<<i<<endl;
    }
    ~B1()
    {
        cout<<"destructing B1"<<endl;
    }
};
//基类 B2
class B2
{
public:
    B2(int j)
    {
        cout<<"constructing B2 "<<j<<endl;
    }
     ~B2()
    {
        cout<<"destructing B2"<<endl;
    }
};
//基类 B3
class B3
{
public:
    B3()
    {
        cout<<"constructing B3"<<endl;
    }
    ~B3()
    {
        cout<<"destructing B3"<<endl;
    }
};
//派生类 C, 继承B2, B1,B3(声明顺序从左至右。 B2->B1->B3)
class C: public B2, public B1, public B3
{
public:
    C(int a, int b, int c, int d):B1(a), memberB2(d), memberB1(c),B2(b)
    {
		//B1,B2的构造函数有参数,B3的构造函数无参数
      	//memberB2(d), memberB1(c)是派生类对自己的数据成员进行初始化的过程、
        //构造函数执行顺序, 基类(声明顺序)-> 内嵌成员对象的构造函数(声明顺序) -> 派生类构造函数中的内容
    }
private:
    B1 memberB1;
    B2 memberB2;
    B3 memberB3;
};
int main() 
{ 
    C obj(1,2,3,4);
    return 0; 
}
/* 输出结果 */
/*
constructing B2 2
constructing B1 1
constructing B3
constructing B1 3
constructing B2 4
constructing B3
destructing B3
destructing B2
destructing B1
destructing B3
destructing B1
destructing B2
*/

二义性问题

在单继承下,基类的public 和protected 成员可以直接被访问,就像它们是派生类的成员一样,对多继承这也是正确的。但是在多继承下,派生类可以从两个或者更多个基类中继承同名的成员。然而在这种情况下,直接访问是二义的,将导致编译时刻错误。
示例:

     
#include <iostream>
using namespace std;
class A
{
public:
void f();
};
class B
{
public:
void f();
void g();
};
class C : public A, public B
{
public:
void g();
void h();
};
int main(){
	C c1;
    // c1.f();    产生二义性问题,访问A中的 f()? or B的 f() ?
    //通过指定成员名,限定消除二义性
    c1.A::f();
    c1.B::f();
}



 
 

     

使用成员名限定法可以消除二义性,但是更好的解决办法是在类C中定义一个同名函数 f(), 类C中的 f() 再根据需要来决定调用 A::f() orB::f(), 这样c1.f() 将调用C::f().

当一个派生类从多个基类派生类,而这些基类又有一个共同的基类,则对该基类中说明的成员进行访问时,也可能会出现二义性。示例:

  
    
//  派生类 B1,B2 继承相同的基类 A, 派生类 C 继承 B1, B2
class A
{
public:
int a;
};
class B1 : public A
{
private:
int b1;
};
class B2 : public A
{
private:
int b2;
};
class C : public B1, public B2
{
public:
int f();
private:
int c;
};
int main(){
	C c1;
    c1.a();
    c1.A::a();
    c1.B1::a();
    c1.B2::a();
	return 0;
}



c1.a; c1.A::a; 这两个访问都有二义性,c1.B1::a; c1.B2::a;是正确的:类C的成员函数 f() 用如下定义可以消除二义性:


       
       
1
2
3
4

      
      
int C::f()
{ 
retrun B1::a + B2::a; 
}

由于二义性的原因,一个类不可以从同一个类中直接继承一次以上。


虚基类

多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如非常经典的菱形继承层次。如下图所示


      
      
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;

类A派生出类B和类C,类D继承自类B和类C,这个时候类A中的成员变量和成员函数继承到类D中变成了两份,一份来自 A–>B–>D 这一路,另一份来自 A–>C–>D 这一条路。当D访问从A中继承的数据时,变一起将无法决定采用哪一条路传过来的数据,于是便出现了虚基类。

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突,而且很少有这样的需求。使用虚基类,可以使得在派生类中只保留间接基类的一份成员。

声明虚基类只需要在继承方式前面加上 virtual 关键字,如下面示例:


#include <iostream>
using namespace std;
class A{
protected:
    int a;
public:
    A(int a):a(a){}
};
class B: virtual public A{  //声明虚基类
protected:
    int b;
public:
    B(int a, int b):A(a),b(b){}
};
class C: virtual public A{  //声明虚基类
protected:
    int c;
public:
    C(int a, int c):A(a),c(c){}
};
class D: virtual public B, virtual public C{  //声明虚基类
private:
    int d;
public:
    D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){}
    void display();
};
void D::display(){
    cout<<"a="<<a<<endl;
    cout<<"b="<<b<<endl;
    cout<<"c="<<c<<endl;
    cout<<"d="<<d<<endl;
}
int main(){
    (new D(1, 2, 3, 4)) -> display();
    return 0;
}
/* 
运行结果:
a=1
b=2
c=3
d=4
*/

  • 本例中我们使用了虚基类,在派生类D中只有一份成员变量 a 的拷贝,所以在 display() 函数中可以直接访问 a,而不用加类名和域解析符。

    虚基类的初始化: 请注意派生类D的构造函数,与以往的用法有所不同。 以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。

    现在,由于虚基类在派生类中只有一份成员变量,所以对这份成员变量的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。

    所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。

    在上述代码中,类D的构造函数通过初始化表调了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了虚基类的构造函数A,这样虚基类的构造函数岂非被调用了3次?

    大家不必过虑,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

    最后请注意: 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。

    赋值兼容原则

    赋值兼容: 赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。

    赋值兼容规则中所指的替代包括:

    • 派生类的对象可以赋值给基类对象;
    • 派生类的对象可以初始化基类的引用;
    • 派生类对象的地址可以赋给指向基类的指针。在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。







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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值