Problem overview
A drawing app keeps rectangles, squares and triangles in one array. You write the shape classes and two methods of Solution. TotalPerimeter(shapes) returns the sum of all the perimeters. LargestShape(shapes) returns the Name() of the shape with the largest perimeter, and on a tie the shape that comes first wins.
For a rectangle 3 by 4, a square of side 5 and a triangle with sides 3, 4 and 5, the perimeters are 14, 20 and 12. The total is 46 and the largest shape is "Square".
Approach: ask the object, never check its kind
You could write TotalPerimeter with a chain of "if it is a rectangle, do this; if it is a square, do that". Every new kind of shape would then force you to edit the method. Polymorphism avoids that. All the shape classes inherit from Shape (: Shape), which declares Name() and Perimeter() as virtual. Each child marks its own versions override. A method that holds a Shape calls shape.Perimeter() and the object runs its own formula.
So the two Solution methods are plain loops over the array. Each looks at every shape once.
Step by step
Shape:Name()returns"Shape"andPerimeter()returns 0. Every real shape overrides both, so these defaults only give the common type its two methods.Rectangle: twopublic intfields,widthandheight. The constructor copies its parameters into them withthis.width = width, because the parameter and the field share a name andthis.picks the field.Perimeteris2 * (width + height).Square: one field,side, andPerimeteris4 * side.Triangle: three fieldsa,b,c, andPerimeteris their sum.TotalPerimeter: starttotalat 0, loop withforeachand addshape.Perimeter()for each shape.LargestShape: start withlargest = shapes[0], loop over all shapes, and replacelargestonly when a shape's perimeter is strictly greater. Returnlargest.Name().
The solution in C#
class Shape {
public virtual string Name() {
return "Shape";
}
public virtual int Perimeter() {
return 0;
}
}
class Rectangle : Shape {
public int width;
public int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public override string Name() {
return "Rectangle";
}
public override int Perimeter() {
return 2 * (width + height);
}
}
class Square : Shape {
public int side;
public Square(int side) {
this.side = side;
}
public override string Name() {
return "Square";
}
public override int Perimeter() {
return 4 * side;
}
}
class Triangle : Shape {
public int a;
public int b;
public int c;
public Triangle(int a, int b, int c) {
this.a = a;
this.b = b;
this.c = c;
}
public override string Name() {
return "Triangle";
}
public override int Perimeter() {
return a + b + c;
}
}
class Solution {
public int TotalPerimeter(Shape[] shapes) {
int total = 0;
foreach (Shape shape in shapes) {
total += shape.Perimeter();
}
return total;
}
public string LargestShape(Shape[] shapes) {
Shape largest = shapes[0];
foreach (Shape shape in shapes) {
if (shape.Perimeter() > largest.Perimeter()) {
largest = shape;
}
}
return largest.Name();
}
}
Details worth knowing
The tie rule is the >. Because only a strictly larger perimeter replaces largest, an equal one never does, and the earlier shape stays. Using >= would return the last shape of a tie instead.
The first shape is compared with itself. That changes nothing, so the loop can begin at the start of the array. The tests never give an empty array, so shapes[0] exists.
The object knows its own name. A 1 by 1 rectangle has the same sides as a square of side 1, but it is still a Rectangle: Name() answers for the object's class, not for its measurements.
Complexity
Each method visits every shape once and calls methods that take constant time, so the time is O(n) for n shapes. Both use a few variables, so the extra space is O(1).
Testing
Three small arrays, run through both methods:
Rectangle(3, 4), Square(5), Triangle(3, 4, 5) -> 46 Square
Rectangle(2, 4), Square(3), Triangle(4, 4, 4) -> 36 Rectangle
Square(1), Rectangle(1, 1), Triangle(2, 2, 3) -> 15 Triangle
Square(1), Rectangle(1, 1) -> 8 Square
In the second line all three perimeters are 12, so the rectangle, first in the array, wins. In the fourth, the square and the rectangle both have perimeter 4, and the square is first.
Tracing LargestShape
Take the first example, a 3 by 4 rectangle, a square of side 5 and a triangle with sides 3, 4 and 5. Start with largest set to the rectangle. Then each shape is compared with the current largest:
| Shape | Perimeter | Replaces largest? | largest afterwards |
|---|---|---|---|
| Rectangle | 14 | No (14 is not above 14) | Rectangle |
| Square | 20 | Yes | Square |
| Triangle | 12 | No | Square |
The method returns "Square". For the second example, all three perimeters are 12, nothing is ever strictly greater than the rectangle's 12, and the answer stays "Rectangle".
A new shape needs no changes
A fifth class, Pentagon, with a side field, Name() returning "Pentagon" and Perimeter() returning 5 * side, works with both methods as they are. For a rectangle 3 by 4, a Pentagon(5) and a Square(5) they return a total of 59 and the largest shape "Pentagon": its 25 beats the square's 20, and the pentagon comes before the square.
Common mistakes
- Checking the kind of a shape with
isor by name insideSolution. It works today and breaks when a new shape appears. - Leaving out
overridein a child class, so the call throughShapereaches the parent's0and"Shape". - Writing
2 * width + heightinstead of2 * (width + height). - Forgetting
this.in a constructor, so the parameter is assigned to itself and the field stays 0.
Why the fields are public ints
Each shape keeps its measurements in public int fields, and the constructor fills them. The perimeter methods read those fields later, so the object must remember its sizes after the constructor ends. Nothing is stored in Shape itself, because a plain shape has no sizes: the sizes belong to the children, and the parent only fixes which methods exist. The sizes are whole numbers up to 10000, so even 100 triangles with the largest sides stay far below the limit of an int.
Key takeaways
- A parent type with
virtualmethods lets one array hold shapes of different kinds and still call their own versions. - Code that works only through the parent's methods keeps working when new kinds appear.
- For "first one wins" on ties, replace the current best only on a strictly greater value.
- Constructors with parameters named like the fields need
this.to store them.
